mirror of
https://github.com/DSpace/dspace-angular.git
synced 2025-10-08 02:24:11 +00:00
@@ -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",
|
||||
|
||||
|
||||
@@ -266,6 +273,8 @@
|
||||
|
||||
"collection.edit.head": "Edit Collection",
|
||||
|
||||
"collection.edit.breadcrumbs": "Edit Collection",
|
||||
|
||||
|
||||
|
||||
"collection.edit.item-mapper.cancel": "Cancel",
|
||||
@@ -450,6 +459,7 @@
|
||||
|
||||
"community.edit.head": "Edit Community",
|
||||
|
||||
"community.edit.breadcrumbs": "Edit Community",
|
||||
|
||||
|
||||
"community.edit.logo.label": "Community logo",
|
||||
@@ -657,6 +667,8 @@
|
||||
|
||||
"item.edit.head": "Edit Item",
|
||||
|
||||
"item.edit.breadcrumbs": "Edit Item",
|
||||
|
||||
|
||||
|
||||
"item.edit.item-mapper.buttons.add": "Map item to selected collections",
|
||||
@@ -1077,6 +1089,8 @@
|
||||
|
||||
"login.title": "Login",
|
||||
|
||||
"login.breadcrumbs": "Login",
|
||||
|
||||
|
||||
|
||||
"logout.form.header": "Log out from DSpace",
|
||||
@@ -1473,6 +1487,7 @@
|
||||
|
||||
"search.title": "DSpace Angular :: Search",
|
||||
|
||||
"search.breadcrumbs": "Search",
|
||||
|
||||
|
||||
"search.filters.applied.f.author": "Author",
|
||||
|
@@ -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';
|
||||
|
41
src/app/+browse-by/browse-by-dso-breadcrumb.resolver.ts
Normal file
41
src/app/+browse-by/browse-by-dso-breadcrumb.resolver.ts
Normal file
@@ -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<BreadcrumbConfig<Community | Collection>> {
|
||||
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;
|
||||
}
|
||||
}
|
@@ -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}"` : ''
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
28
src/app/+browse-by/browse-by-i18n-breadcrumb.resolver.ts
Normal file
28
src/app/+browse-by/browse-by-i18n-breadcrumb.resolver.ts
Normal file
@@ -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<string> {
|
||||
const extendedBreadcrumbKey = route.data.breadcrumbKey + '.' + route.params.id;
|
||||
route.data = Object.assign({}, route.data, { breadcrumbKey: extendedBreadcrumbKey });
|
||||
return super.resolve(route, state);
|
||||
}
|
||||
}
|
@@ -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 {
|
||||
|
@@ -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(),
|
||||
|
@@ -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<RemoteData<Collection>>;
|
||||
this.collectionRD$ = this.route.data.pipe(map((data) => data.dso)).pipe(getSucceededRemoteData()) as Observable<RemoteData<Collection>>;
|
||||
this.searchOptions$ = this.searchConfigService.paginatedSearchOptions;
|
||||
this.loadItemLists();
|
||||
}
|
||||
|
@@ -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';
|
||||
|
||||
@@ -18,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() {
|
||||
@@ -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: COLLECTION_CREATE_PATH,
|
||||
component: CreateCollectionPageComponent,
|
||||
canActivate: [AuthenticatedGuard, CreateCollectionPageGuard]
|
||||
path: ':id',
|
||||
resolve: {
|
||||
dso: CollectionPageResolver,
|
||||
breadcrumb: CollectionBreadcrumbResolver
|
||||
},
|
||||
children: [
|
||||
{
|
||||
path: COLLECTION_EDIT_PATH,
|
||||
loadChildren: './edit-collection-page/edit-collection-page.module#EditCollectionPageModule',
|
||||
canActivate: [AuthenticatedGuard]
|
||||
},
|
||||
{
|
||||
path: ':id/delete',
|
||||
path: 'delete',
|
||||
pathMatch: 'full',
|
||||
component: DeleteCollectionPageComponent,
|
||||
canActivate: [AuthenticatedGuard],
|
||||
resolve: {
|
||||
dso: CollectionPageResolver
|
||||
}
|
||||
},
|
||||
{
|
||||
path: ':id',
|
||||
path: '',
|
||||
component: CollectionPageComponent,
|
||||
pathMatch: 'full',
|
||||
resolve: {
|
||||
collection: CollectionPageResolver
|
||||
}
|
||||
},
|
||||
{
|
||||
path: ':id/edit/mapper',
|
||||
path: '/edit/mapper',
|
||||
component: CollectionItemMapperComponent,
|
||||
pathMatch: 'full',
|
||||
resolve: {
|
||||
collection: CollectionPageResolver
|
||||
},
|
||||
canActivate: [AuthenticatedGuard]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
path: COLLECTION_CREATE_PATH,
|
||||
component: CreateCollectionPageComponent,
|
||||
canActivate: [AuthenticatedGuard, CreateCollectionPageGuard]
|
||||
},
|
||||
])
|
||||
],
|
||||
providers: [
|
||||
CollectionPageResolver,
|
||||
CollectionBreadcrumbResolver,
|
||||
DSOBreadcrumbsService,
|
||||
LinkService,
|
||||
CreateCollectionPageGuard
|
||||
]
|
||||
})
|
||||
|
@@ -62,7 +62,7 @@ export class CollectionPageComponent implements OnInit {
|
||||
|
||||
ngOnInit(): void {
|
||||
this.collectionRD$ = this.route.data.pipe(
|
||||
map((data) => data.collection as RemoteData<Collection>),
|
||||
map((data) => data.dso as RemoteData<Collection>),
|
||||
redirectToPageNotFoundOn404(this.router),
|
||||
take(1)
|
||||
);
|
||||
|
@@ -1,11 +1,11 @@
|
||||
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';
|
||||
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
|
||||
@@ -15,10 +15,11 @@ import { CollectionCurateComponent } from './collection-curate/collection-curate
|
||||
RouterModule.forChild([
|
||||
{
|
||||
path: '',
|
||||
component: EditCollectionPageComponent,
|
||||
resolve: {
|
||||
dso: CollectionPageResolver
|
||||
breadcrumb: I18nBreadcrumbResolver
|
||||
},
|
||||
data: { breadcrumbKey: 'collection.edit' },
|
||||
component: EditCollectionPageComponent,
|
||||
children: [
|
||||
{
|
||||
path: '',
|
||||
@@ -30,30 +31,28 @@ import { CollectionCurateComponent } from './collection-curate/collection-curate
|
||||
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 }
|
||||
}
|
||||
]
|
||||
}
|
||||
])
|
||||
],
|
||||
providers: [
|
||||
CollectionPageResolver,
|
||||
]
|
||||
})
|
||||
export class EditCollectionPageRoutingModule {
|
||||
|
@@ -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';
|
||||
|
||||
@@ -17,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() {
|
||||
@@ -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: COMMUNITY_CREATE_PATH,
|
||||
component: CreateCommunityPageComponent,
|
||||
canActivate: [AuthenticatedGuard, CreateCommunityPageGuard]
|
||||
path: ':id',
|
||||
resolve: {
|
||||
dso: CommunityPageResolver,
|
||||
breadcrumb: CommunityBreadcrumbResolver
|
||||
},
|
||||
children: [
|
||||
{
|
||||
path: COMMUNITY_EDIT_PATH,
|
||||
loadChildren: './edit-community-page/edit-community-page.module#EditCommunityPageModule',
|
||||
canActivate: [AuthenticatedGuard]
|
||||
},
|
||||
{
|
||||
path: ':id/delete',
|
||||
path: 'delete',
|
||||
pathMatch: 'full',
|
||||
component: DeleteCommunityPageComponent,
|
||||
canActivate: [AuthenticatedGuard],
|
||||
resolve: {
|
||||
dso: CommunityPageResolver
|
||||
}
|
||||
},
|
||||
{
|
||||
path: ':id',
|
||||
path: '',
|
||||
component: CommunityPageComponent,
|
||||
pathMatch: 'full',
|
||||
resolve: {
|
||||
community: CommunityPageResolver
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
path: COMMUNITY_CREATE_PATH,
|
||||
component: CreateCommunityPageComponent,
|
||||
canActivate: [AuthenticatedGuard, CreateCommunityPageGuard]
|
||||
},
|
||||
])
|
||||
],
|
||||
providers: [
|
||||
CommunityPageResolver,
|
||||
CommunityBreadcrumbResolver,
|
||||
DSOBreadcrumbsService,
|
||||
LinkService,
|
||||
CreateCommunityPageGuard
|
||||
]
|
||||
})
|
||||
|
@@ -46,7 +46,7 @@ export class CommunityPageComponent implements OnInit {
|
||||
|
||||
ngOnInit(): void {
|
||||
this.communityRD$ = this.route.data.pipe(
|
||||
map((data) => data.community as RemoteData<Community>),
|
||||
map((data) => data.dso as RemoteData<Community>),
|
||||
redirectToPageNotFoundOn404(this.router)
|
||||
);
|
||||
this.logoRD$ = this.communityRD$.pipe(
|
||||
|
@@ -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,10 +15,11 @@ import { CommunityCurateComponent } from './community-curate/community-curate.co
|
||||
RouterModule.forChild([
|
||||
{
|
||||
path: '',
|
||||
component: EditCommunityPageComponent,
|
||||
resolve: {
|
||||
dso: CommunityPageResolver
|
||||
breadcrumb: I18nBreadcrumbResolver
|
||||
},
|
||||
data: { breadcrumbKey: 'community.edit' },
|
||||
component: EditCommunityPageComponent,
|
||||
children: [
|
||||
{
|
||||
path: '',
|
||||
@@ -29,26 +31,24 @@ import { CommunityCurateComponent } from './community-curate/community-curate.co
|
||||
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 }
|
||||
}
|
||||
]
|
||||
}
|
||||
])
|
||||
],
|
||||
providers: [
|
||||
CommunityPageResolver,
|
||||
]
|
||||
})
|
||||
export class EditCommunityPageRoutingModule {
|
||||
|
||||
|
@@ -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,10 +30,14 @@ const ITEM_EDIT_MOVE_PATH = 'move';
|
||||
RouterModule.forChild([
|
||||
{
|
||||
path: '',
|
||||
component: EditItemPageComponent,
|
||||
resolve: {
|
||||
item: ItemPageResolver
|
||||
breadcrumb: I18nBreadcrumbResolver
|
||||
},
|
||||
data: { breadcrumbKey: 'item.edit' },
|
||||
children: [
|
||||
{
|
||||
path: '',
|
||||
component: EditItemPageComponent,
|
||||
children: [
|
||||
{
|
||||
path: '',
|
||||
@@ -42,91 +47,71 @@ 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 }
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
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 {
|
||||
|
||||
|
@@ -7,44 +7,55 @@ 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();
|
||||
}
|
||||
|
||||
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 = ':id/edit';
|
||||
const ITEM_EDIT_PATH = 'edit';
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
RouterModule.forChild([
|
||||
{
|
||||
path: ':id',
|
||||
resolve: {
|
||||
item: ItemPageResolver,
|
||||
breadcrumb: ItemBreadcrumbResolver
|
||||
},
|
||||
children: [
|
||||
{
|
||||
path: '',
|
||||
component: ItemPageComponent,
|
||||
pathMatch: 'full',
|
||||
resolve: {
|
||||
item: ItemPageResolver
|
||||
}
|
||||
},
|
||||
{
|
||||
path: ':id/full',
|
||||
path: 'full',
|
||||
component: FullItemPageComponent,
|
||||
resolve: {
|
||||
item: ItemPageResolver
|
||||
}
|
||||
},
|
||||
{
|
||||
path: ITEM_EDIT_PATH,
|
||||
loadChildren: './edit-item-page/edit-item-page.module#EditItemPageModule',
|
||||
canActivate: [AuthenticatedGuard]
|
||||
},
|
||||
}
|
||||
],
|
||||
}
|
||||
])
|
||||
],
|
||||
providers: [
|
||||
ItemPageResolver,
|
||||
ItemBreadcrumbResolver,
|
||||
DSOBreadcrumbsService,
|
||||
LinkService
|
||||
]
|
||||
|
||||
})
|
||||
export class ItemPageRoutingModule {
|
||||
|
||||
|
@@ -27,7 +27,7 @@ export class ItemPageResolver implements Resolve<RemoteData<Item>> {
|
||||
return this.itemService.findById(route.params.id,
|
||||
followLink('owningCollection'),
|
||||
followLink('bundles'),
|
||||
followLink('relationships')
|
||||
followLink('relationships'),
|
||||
).pipe(
|
||||
find((RD) => hasValue(RD.error) || RD.hasSucceeded),
|
||||
);
|
||||
|
@@ -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: 'login', title: 'login.title' } }
|
||||
])
|
||||
]
|
||||
})
|
||||
export class LoginPageRoutingModule { }
|
||||
export class LoginPageRoutingModule {
|
||||
}
|
||||
|
@@ -4,13 +4,24 @@ 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 { 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' } },
|
||||
RouterModule.forChild([{
|
||||
path: '',
|
||||
resolve: { breadcrumb: I18nBreadcrumbResolver }, data: { title: 'search.title', breadcrumbKey: 'search' },
|
||||
children: [
|
||||
{ path: '', component: SearchPageComponent },
|
||||
{ path: ':configuration', component: ConfigurationSearchPageComponent, canActivate: [ConfigurationSearchPageGuard] }
|
||||
])
|
||||
]
|
||||
}]
|
||||
)
|
||||
],
|
||||
providers: [
|
||||
I18nBreadcrumbResolver,
|
||||
I18nBreadcrumbsService
|
||||
]
|
||||
})
|
||||
export class SearchPageRoutingModule {
|
||||
|
@@ -3,30 +3,56 @@ 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';
|
||||
import { BrowseByDSOBreadcrumbResolver } from './+browse-by/browse-by-dso-breadcrumb.resolver';
|
||||
|
||||
const ITEM_MODULE_PATH = 'items';
|
||||
|
||||
export function getItemModulePath() {
|
||||
return `/${ITEM_MODULE_PATH}`;
|
||||
}
|
||||
|
||||
const COLLECTION_MODULE_PATH = 'collections';
|
||||
|
||||
export function getCollectionModulePath() {
|
||||
return `/${COLLECTION_MODULE_PATH}`;
|
||||
}
|
||||
|
||||
const COMMUNITY_MODULE_PATH = 'communities';
|
||||
|
||||
export function getCommunityModulePath() {
|
||||
return `/${COMMUNITY_MODULE_PATH}`;
|
||||
}
|
||||
|
||||
const ADMIN_MODULE_PATH = 'admin';
|
||||
|
||||
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([
|
||||
{ path: '', redirectTo: '/home', pathMatch: 'full' },
|
||||
{ path: 'home', loadChildren: './+home-page/home-page.module#HomePageModule' },
|
||||
{ path: 'home', loadChildren: './+home-page/home-page.module#HomePageModule', data: { showBreadcrumbs: false } },
|
||||
{ path: 'community-list', loadChildren: './community-list-page/community-list-page.module#CommunityListPageModule' },
|
||||
{ path: 'id', loadChildren: './+lookup-by-id/lookup-by-id.module#LookupIdModule' },
|
||||
{ path: 'handle', loadChildren: './+lookup-by-id/lookup-by-id.module#LookupIdModule' },
|
||||
@@ -45,7 +71,7 @@ export function getAdminModulePath() {
|
||||
{ path: '**', pathMatch: 'full', component: PageNotFoundComponent },
|
||||
])
|
||||
],
|
||||
exports: [RouterModule]
|
||||
exports: [RouterModule],
|
||||
})
|
||||
export class AppRoutingModule {
|
||||
|
||||
|
@@ -10,6 +10,10 @@
|
||||
[options]="config.notifications">
|
||||
</ds-notifications-board>
|
||||
<main class="main-content">
|
||||
<div class="container">
|
||||
<ds-breadcrumbs></ds-breadcrumbs>
|
||||
</div>
|
||||
|
||||
<div class="container" *ngIf="isLoading$ | async">
|
||||
<ds-loading message="{{'loading.default' | translate}}"></ds-loading>
|
||||
</div>
|
||||
|
@@ -38,6 +38,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
|
||||
|
21
src/app/breadcrumbs/breadcrumb/breadcrumb-config.model.ts
Normal file
21
src/app/breadcrumbs/breadcrumb/breadcrumb-config.model.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import { BreadcrumbsService } from '../../core/breadcrumbs/breadcrumbs.service';
|
||||
|
||||
/**
|
||||
* Interface for breadcrumb configuration objects
|
||||
*/
|
||||
export interface BreadcrumbConfig<T> {
|
||||
/**
|
||||
* The service used to calculate the breadcrumb object
|
||||
*/
|
||||
provider: BreadcrumbsService<T>;
|
||||
|
||||
/**
|
||||
* The key that is used to calculate the breadcrumb display value
|
||||
*/
|
||||
key: T;
|
||||
|
||||
/**
|
||||
* The url of the breadcrumb
|
||||
*/
|
||||
url?: string;
|
||||
}
|
15
src/app/breadcrumbs/breadcrumb/breadcrumb.model.ts
Normal file
15
src/app/breadcrumbs/breadcrumb/breadcrumb.model.ts
Normal file
@@ -0,0 +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) {
|
||||
}
|
||||
}
|
17
src/app/breadcrumbs/breadcrumbs.component.html
Normal file
17
src/app/breadcrumbs/breadcrumbs.component.html
Normal file
@@ -0,0 +1,17 @@
|
||||
<nav *ngIf="showBreadcrumbs" aria-label="breadcrumb">
|
||||
<ol class="breadcrumb">
|
||||
<ng-container *ngTemplateOutlet="breadcrumbs.length > 0 ? breadcrumb : activeBreadcrumb; context: {text: 'Home', url: '/'}"></ng-container>
|
||||
<ng-container *ngFor="let bc of breadcrumbs; let last = last;">
|
||||
<ng-container *ngTemplateOutlet="!last ? breadcrumb : activeBreadcrumb; context: bc"></ng-container>
|
||||
</ng-container>
|
||||
</ol>
|
||||
</nav>
|
||||
|
||||
<ng-template #breadcrumb let-text="text" let-url="url">
|
||||
<li class="breadcrumb-item"><a [routerLink]="url">{{text | translate}}</a></li>
|
||||
</ng-template>
|
||||
|
||||
<ng-template #activeBreadcrumb let-text="text" >
|
||||
<li class="breadcrumb-item active" aria-current="page">{{text | translate}}</li>
|
||||
</ng-template>
|
||||
|
0
src/app/breadcrumbs/breadcrumbs.component.scss
Normal file
0
src/app/breadcrumbs/breadcrumbs.component.scss
Normal file
111
src/app/breadcrumbs/breadcrumbs.component.spec.ts
Normal file
111
src/app/breadcrumbs/breadcrumbs.component.spec.ts
Normal file
@@ -0,0 +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<string> {
|
||||
getBreadcrumbs(key: string, url: string): Observable<Breadcrumb[]> {
|
||||
return observableOf([new Breadcrumb(key, url)]);
|
||||
}
|
||||
}
|
||||
|
||||
describe('BreadcrumbsComponent', () => {
|
||||
let component: BreadcrumbsComponent;
|
||||
let fixture: ComponentFixture<BreadcrumbsComponent>;
|
||||
let router: any;
|
||||
let route: any;
|
||||
let breadcrumbProvider;
|
||||
let breadcrumbConfigA: BreadcrumbConfig<string>;
|
||||
let breadcrumbConfigB: BreadcrumbConfig<string>;
|
||||
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],
|
||||
imports: [RouterTestingModule.withRoutes([]), TranslateModule.forRoot({
|
||||
loader: {
|
||||
provide: TranslateLoader,
|
||||
useClass: MockTranslateLoader
|
||||
}
|
||||
})],
|
||||
providers: [
|
||||
{ provide: ActivatedRoute, useValue: route }
|
||||
|
||||
]
|
||||
})
|
||||
.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 })
|
||||
})
|
||||
})
|
||||
});
|
100
src/app/breadcrumbs/breadcrumbs.component.ts
Normal file
100
src/app/breadcrumbs/breadcrumbs.component.ts
Normal file
@@ -0,0 +1,100 @@
|
||||
import { Component, OnDestroy, OnInit } from '@angular/core';
|
||||
import { ActivatedRoute, NavigationEnd, Router } from '@angular/router';
|
||||
import { Breadcrumb } from './breadcrumb/breadcrumb.model';
|
||||
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';
|
||||
|
||||
/**
|
||||
* 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(
|
||||
private route: ActivatedRoute,
|
||||
private router: Router
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the breadcrumbs on init for this page
|
||||
*/
|
||||
ngOnInit(): void {
|
||||
this.subscription = this.router.events.pipe(
|
||||
filter((e): e is NavigationEnd => e instanceof NavigationEnd),
|
||||
tap(() => this.reset()),
|
||||
switchMap(() => this.resolveBreadcrumbs(this.route.root))
|
||||
).subscribe((breadcrumbs) => {
|
||||
this.breadcrumbs = breadcrumbs;
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Method that recursively resolves breadcrumbs
|
||||
* @param route The route to get the breadcrumb from
|
||||
*/
|
||||
resolveBreadcrumbs(route: ActivatedRoute): Observable<Breadcrumb[]> {
|
||||
const data = route.snapshot.data;
|
||||
const routeConfig = route.snapshot.routeConfig;
|
||||
|
||||
const last: boolean = hasNoValue(route.firstChild);
|
||||
if (last) {
|
||||
if (hasValue(data.showBreadcrumbs)) {
|
||||
this.showBreadcrumbs = data.showBreadcrumbs;
|
||||
} else if (isUndefined(data.breadcrumb)) {
|
||||
this.showBreadcrumbs = false;
|
||||
}
|
||||
}
|
||||
|
||||
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.resolveBreadcrumbs(route.firstChild))
|
||||
.pipe(map((crumbs) => [].concat.apply([], crumbs)));
|
||||
} else {
|
||||
return provider.getBreadcrumbs(key, url);
|
||||
}
|
||||
}
|
||||
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;
|
||||
}
|
||||
}
|
15
src/app/core/breadcrumbs/breadcrumbs.service.ts
Normal file
15
src/app/core/breadcrumbs/breadcrumbs.service.ts
Normal file
@@ -0,0 +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<T> {
|
||||
|
||||
/**
|
||||
* 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<Breadcrumb[]>;
|
||||
}
|
29
src/app/core/breadcrumbs/collection-breadcrumb.resolver.ts
Normal file
29
src/app/core/breadcrumbs/collection-breadcrumb.resolver.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
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
|
||||
*/
|
||||
@Injectable()
|
||||
export class CollectionBreadcrumbResolver extends DSOBreadcrumbResolver<Collection> {
|
||||
constructor(protected breadcrumbService: DSOBreadcrumbsService, protected dataService: CollectionDataService) {
|
||||
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<FollowLinkConfig<Collection>> {
|
||||
return [
|
||||
followLink('parentCommunity', undefined,
|
||||
followLink('parentCommunity')
|
||||
)
|
||||
];
|
||||
}
|
||||
}
|
27
src/app/core/breadcrumbs/community-breadcrumb.resolver.ts
Normal file
27
src/app/core/breadcrumbs/community-breadcrumb.resolver.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
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';
|
||||
import { followLink, FollowLinkConfig } from '../../shared/utils/follow-link-config.model';
|
||||
|
||||
/**
|
||||
* The class that resolves the BreadcrumbConfig object for a Community
|
||||
*/
|
||||
@Injectable()
|
||||
export class CommunityBreadcrumbResolver extends DSOBreadcrumbResolver<Community> {
|
||||
constructor(protected breadcrumbService: DSOBreadcrumbsService, protected dataService: CommunityDataService) {
|
||||
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<FollowLinkConfig<Community>> {
|
||||
return [
|
||||
followLink('parentCommunity')
|
||||
];
|
||||
}
|
||||
}
|
35
src/app/core/breadcrumbs/dso-breadcrumb.resolver.spec.ts
Normal file
35
src/app/core/breadcrumbs/dso-breadcrumb.resolver.spec.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
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', () => {
|
||||
let resolver: DSOBreadcrumbResolver<Collection>;
|
||||
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 CollectionBreadcrumbResolver(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})
|
||||
});
|
||||
});
|
||||
});
|
46
src/app/core/breadcrumbs/dso-breadcrumb.resolver.ts
Normal file
46
src/app/core/breadcrumbs/dso-breadcrumb.resolver.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
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';
|
||||
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';
|
||||
import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model';
|
||||
|
||||
/**
|
||||
* The class that resolves the BreadcrumbConfig object for a DSpaceObject
|
||||
*/
|
||||
@Injectable()
|
||||
export abstract class DSOBreadcrumbResolver<T extends ChildHALResource & DSpaceObject> implements Resolve<BreadcrumbConfig<T>> {
|
||||
constructor(protected breadcrumbService: DSOBreadcrumbsService, protected dataService: DataService<T>) {
|
||||
}
|
||||
|
||||
/**
|
||||
* 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<BreadcrumbConfig<T>> {
|
||||
const uuid = route.params.id;
|
||||
return this.dataService.findById(uuid, ...this.followLinks).pipe(
|
||||
getSucceededRemoteData(),
|
||||
getRemoteDataPayload(),
|
||||
map((object: T) => {
|
||||
const fullPath = state.url;
|
||||
const url = fullPath.substr(0, fullPath.indexOf(uuid)) + uuid;
|
||||
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<FollowLinkConfig<T>>;
|
||||
}
|
122
src/app/core/breadcrumbs/dso-breadcrumbs.service.spec.ts
Normal file
122
src/app/core/breadcrumbs/dso-breadcrumbs.service.spec.ts
Normal file
@@ -0,0 +1,122 @@
|
||||
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 { of as observableOf } from 'rxjs';
|
||||
import { Community } from '../shared/community.model';
|
||||
import { Collection } from '../shared/collection.model';
|
||||
import { Breadcrumb } from '../../breadcrumbs/breadcrumb/breadcrumb.model';
|
||||
import { getTestScheduler } from 'jasmine-marbles';
|
||||
import { getDSOPath } from '../../app-routing.module';
|
||||
import { DSONameService } from './dso-name.service';
|
||||
|
||||
describe('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 dsoNameService;
|
||||
|
||||
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',
|
||||
metadata: {
|
||||
'dc.title': [{value: 'community'}]
|
||||
},
|
||||
uuid: communityUUID,
|
||||
parentCommunity: observableOf(Object.assign(createSuccessfulRemoteDataObject(undefined), { statusCode: 204 })),
|
||||
|
||||
_links: {
|
||||
parentCommunity: 'site',
|
||||
self: communityPath + communityUUID
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
testCollection = Object.assign(new Collection(),
|
||||
{
|
||||
type: 'collection',
|
||||
metadata: {
|
||||
'dc.title': [{value: 'collection'}]
|
||||
},
|
||||
uuid: collectionUUID,
|
||||
parentCommunity: createSuccessfulRemoteDataObject$(testCommunity),
|
||||
_links: {
|
||||
parentCommunity: communityPath + communityUUID,
|
||||
self: communityPath + collectionUUID
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
testItem = Object.assign(new Item(),
|
||||
{
|
||||
type: 'item',
|
||||
metadata: {
|
||||
'dc.title': [{value: 'item'}]
|
||||
},
|
||||
uuid: itemUUID,
|
||||
owningCollection: createSuccessfulRemoteDataObject$(testCollection),
|
||||
_links: {
|
||||
owningCollection: collectionPath + collectionUUID,
|
||||
self: itemPath + itemUUID
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
dsoNameService = { getName: (dso) => getName(dso) }
|
||||
}
|
||||
|
||||
beforeEach(async(() => {
|
||||
init();
|
||||
TestBed.configureTestingModule({
|
||||
providers: [
|
||||
{ provide: LinkService, useValue: getMockLinkService() },
|
||||
{ provide: DSONameService, useValue: dsoNameService }
|
||||
]
|
||||
}).compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
linkService = TestBed.get(LinkService);
|
||||
linkService.resolveLink.and.callFake((object, link) => object);
|
||||
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(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
|
||||
}
|
||||
});
|
50
src/app/core/breadcrumbs/dso-breadcrumbs.service.ts
Normal file
50
src/app/core/breadcrumbs/dso-breadcrumbs.service.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
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 { 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';
|
||||
|
||||
/**
|
||||
* Service to calculate DSpaceObject breadcrumbs for a single part of the route
|
||||
*/
|
||||
@Injectable()
|
||||
export class DSOBreadcrumbsService implements BreadcrumbsService<ChildHALResource & DSpaceObject> {
|
||||
constructor(
|
||||
private linkService: LinkService,
|
||||
private dsoNameService: DSONameService
|
||||
) {
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Method to recursively calculate the breadcrumbs
|
||||
* This method returns the name and url of the key and all its parent DSO's recursively, top down
|
||||
* @param key The key (a DSpaceObject) used to resolve the breadcrumb
|
||||
* @param url The url to use as a link for this breadcrumb
|
||||
*/
|
||||
getBreadcrumbs(key: ChildHALResource & DSpaceObject, url: string): Observable<Breadcrumb[]> {
|
||||
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((parentRD: RemoteData<ChildHALResource & DSpaceObject>) => parentRD.hasSucceeded || parentRD.statusCode === 204),
|
||||
switchMap((parentRD: RemoteData<ChildHALResource & DSpaceObject>) => {
|
||||
if (hasValue(parentRD.payload)) {
|
||||
const parent = parentRD.payload;
|
||||
return this.getBreadcrumbs(parent, getDSOPath(parent))
|
||||
}
|
||||
return observableOf([]);
|
||||
|
||||
}),
|
||||
map((breadcrumbs: Breadcrumb[]) => [...breadcrumbs, crumb])
|
||||
);
|
||||
}
|
||||
}
|
116
src/app/core/breadcrumbs/dso-name.service.spec.ts
Normal file
116
src/app/core/breadcrumbs/dso-name.service.spec.ts
Normal file
@@ -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<string | GenericConstructor<ListableObject>> {
|
||||
return ['Person', Item, DSpaceObject];
|
||||
}
|
||||
});
|
||||
|
||||
mockOrgUnitName = 'Molecular Spectroscopy';
|
||||
mockOrgUnit = Object.assign(new DSpaceObject(), {
|
||||
firstMetadataValue(keyOrKeys: string | string[], valueFilter?: MetadataValueFilter): string {
|
||||
return mockOrgUnitName
|
||||
},
|
||||
getRenderTypes(): Array<string | GenericConstructor<ListableObject>> {
|
||||
return ['OrgUnit', Item, DSpaceObject];
|
||||
}
|
||||
});
|
||||
|
||||
mockDSOName = 'Lorem Ipsum';
|
||||
mockDSO = Object.assign(new DSpaceObject(), {
|
||||
firstMetadataValue(keyOrKeys: string | string[], valueFilter?: MetadataValueFilter): string {
|
||||
return mockDSOName
|
||||
},
|
||||
getRenderTypes(): Array<string | GenericConstructor<ListableObject>> {
|
||||
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');
|
||||
});
|
||||
});
|
||||
});
|
53
src/app/core/breadcrumbs/dso-name.service.ts
Normal file
53
src/app/core/breadcrumbs/dso-name.service.ts
Normal file
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
28
src/app/core/breadcrumbs/i18n-breadcrumb.resolver.spec.ts
Normal file
28
src/app/core/breadcrumbs/i18n-breadcrumb.resolver.spec.ts
Normal file
@@ -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();
|
||||
});
|
||||
});
|
||||
});
|
29
src/app/core/breadcrumbs/i18n-breadcrumb.resolver.ts
Normal file
29
src/app/core/breadcrumbs/i18n-breadcrumb.resolver.ts
Normal file
@@ -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 resolves a BreadcrumbConfig object with an i18n key string for a route
|
||||
*/
|
||||
@Injectable()
|
||||
export class I18nBreadcrumbResolver implements Resolve<BreadcrumbConfig<string>> {
|
||||
constructor(protected breadcrumbService: I18nBreadcrumbsService) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Method for resolving an I18n breadcrumb configuration object
|
||||
* @param {ActivatedRouteSnapshot} route The current ActivatedRouteSnapshot
|
||||
* @param {RouterStateSnapshot} state The current RouterStateSnapshot
|
||||
* @returns BreadcrumbConfig object
|
||||
*/
|
||||
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')
|
||||
}
|
||||
const fullPath = route.url.join('');
|
||||
return { provider: this.breadcrumbService, key: key, url: fullPath };
|
||||
}
|
||||
}
|
31
src/app/core/breadcrumbs/i18n-breadcrumbs.service.spec.ts
Normal file
31
src/app/core/breadcrumbs/i18n-breadcrumbs.service.spec.ts
Normal file
@@ -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';
|
||||
|
||||
describe('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)] });
|
||||
})
|
||||
});
|
||||
});
|
25
src/app/core/breadcrumbs/i18n-breadcrumbs.service.ts
Normal file
25
src/app/core/breadcrumbs/i18n-breadcrumbs.service.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import { Breadcrumb } from '../../breadcrumbs/breadcrumb/breadcrumb.model';
|
||||
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<string> {
|
||||
|
||||
/**
|
||||
* 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<Breadcrumb[]> {
|
||||
return observableOf([new Breadcrumb(key + BREADCRUMB_MESSAGE_POSTFIX, url)]);
|
||||
}
|
||||
}
|
32
src/app/core/breadcrumbs/item-breadcrumb.resolver.ts
Normal file
32
src/app/core/breadcrumbs/item-breadcrumb.resolver.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
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 { followLink, FollowLinkConfig } from '../../shared/utils/follow-link-config.model';
|
||||
|
||||
/**
|
||||
* The class that resolves the BreadcrumbConfig object for an Item
|
||||
*/
|
||||
@Injectable()
|
||||
export class ItemBreadcrumbResolver extends DSOBreadcrumbResolver<Item> {
|
||||
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<FollowLinkConfig<Item>> {
|
||||
return [
|
||||
followLink('owningCollection', undefined,
|
||||
followLink('parentCommunity', undefined,
|
||||
followLink('parentCommunity'))
|
||||
),
|
||||
followLink('bundles'),
|
||||
followLink('relationships')
|
||||
];
|
||||
}
|
||||
}
|
@@ -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
|
||||
|
||||
);
|
||||
})
|
||||
);
|
||||
|
@@ -17,7 +17,8 @@ export class RemoteData<T> {
|
||||
private responsePending?: boolean,
|
||||
private isSuccessful?: boolean,
|
||||
public error?: RemoteDataError,
|
||||
public payload?: T
|
||||
public payload?: T,
|
||||
public statusCode?: number,
|
||||
) {
|
||||
}
|
||||
|
||||
|
@@ -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 <meta name="title" ... > to the <head>
|
||||
*/
|
||||
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);
|
||||
}
|
||||
|
12
src/app/core/shared/child-hal-resource.model.ts
Normal file
12
src/app/core/shared/child-hal-resource.model.ts
Normal file
@@ -0,0 +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'];
|
||||
}
|
@@ -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';
|
||||
}
|
||||
}
|
||||
|
@@ -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';
|
||||
}
|
||||
}
|
||||
|
@@ -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;
|
||||
|
@@ -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';
|
||||
}
|
||||
}
|
||||
|
@@ -23,16 +23,16 @@ describe('OrgUnitItemMetadataListElementComponent', () => {
|
||||
declarations: [OrgUnitItemMetadataListElementComponent],
|
||||
schemas: [NO_ERRORS_SCHEMA]
|
||||
}).overrideComponent(OrgUnitItemMetadataListElementComponent, {
|
||||
// set: { changeDetection: ChangeDetectionStrategy.Default }
|
||||
set: { changeDetection: ChangeDetectionStrategy.Default }
|
||||
}).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;
|
||||
|
@@ -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;
|
||||
|
@@ -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)
|
||||
);
|
||||
}
|
||||
|
||||
|
Reference in New Issue
Block a user