[CST4981] finished task, working on unit testing

This commit is contained in:
Rezart Vata
2021-12-22 18:01:37 +01:00
parent e86afacff1
commit e594cabe4a
17 changed files with 256 additions and 103 deletions

View File

@@ -21,6 +21,7 @@ import { MenuService } from '../../shared/menu/menu.service';
import { CSSVariableService } from '../../shared/sass-helper/sass-helper.service'; import { CSSVariableService } from '../../shared/sass-helper/sass-helper.service';
import { AuthorizationDataService } from '../../core/data/feature-authorization/authorization-data.service'; import { AuthorizationDataService } from '../../core/data/feature-authorization/authorization-data.service';
import { FeatureID } from '../../core/data/feature-authorization/feature-id'; import { FeatureID } from '../../core/data/feature-authorization/feature-id';
import { Router, ActivatedRoute } from '@angular/router';
/** /**
* Component representing the admin sidebar * Component representing the admin sidebar
@@ -63,14 +64,15 @@ export class AdminSidebarComponent extends MenuComponent implements OnInit {
inFocus$: BehaviorSubject<boolean>; inFocus$: BehaviorSubject<boolean>;
constructor(protected menuService: MenuService, constructor(protected menuService: MenuService,
protected injector: Injector, protected injector: Injector,
private variableService: CSSVariableService, private variableService: CSSVariableService,
private authService: AuthService, private authService: AuthService,
private modalService: NgbModal, private modalService: NgbModal,
private authorizationService: AuthorizationDataService, public authorizationService: AuthorizationDataService,
private scriptDataService: ScriptDataService, private scriptDataService: ScriptDataService,
public route: ActivatedRoute
) { ) {
super(menuService, injector); super(menuService, injector, authorizationService, route);
this.inFocus$ = new BehaviorSubject(false); this.inFocus$ = new BehaviorSubject(false);
} }
@@ -144,7 +146,7 @@ export class AdminSidebarComponent extends MenuComponent implements OnInit {
type: MenuItemType.TEXT, type: MenuItemType.TEXT,
text: 'menu.section.new' text: 'menu.section.new'
} as TextMenuItemModel, } as TextMenuItemModel,
icon: 'plus', icon: 'plus',
index: 0 index: 0
}, },
{ {

View File

@@ -72,6 +72,7 @@ import { ThemedCollectionPageComponent } from './themed-collection-page.componen
id: 'statistics_collection_:id', id: 'statistics_collection_:id',
active: true, active: true,
visible: true, visible: true,
type: 'statistics',
model: { model: {
type: MenuItemType.LINK, type: MenuItemType.LINK,
text: 'menu.section.statistics', text: 'menu.section.statistics',

View File

@@ -103,20 +103,20 @@ export class CollectionPageComponent implements OnInit {
const currentSort$ = this.paginationService.getCurrentSort(this.paginationConfig.id, this.sortConfig); const currentSort$ = this.paginationService.getCurrentSort(this.paginationConfig.id, this.sortConfig);
this.itemRD$ = observableCombineLatest([currentPagination$, currentSort$]).pipe( this.itemRD$ = observableCombineLatest([currentPagination$, currentSort$]).pipe(
switchMap(([currentPagination, currentSort ]) => this.collectionRD$.pipe( switchMap(([currentPagination, currentSort]) => this.collectionRD$.pipe(
getFirstSucceededRemoteData(), getFirstSucceededRemoteData(),
map((rd) => rd.payload.id), map((rd) => rd.payload.id),
switchMap((id: string) => { switchMap((id: string) => {
return this.searchService.search( return this.searchService.search(
new PaginatedSearchOptions({ new PaginatedSearchOptions({
scope: id, scope: id,
pagination: currentPagination, pagination: currentPagination,
sort: currentSort, sort: currentSort,
dsoTypes: [DSpaceObjectType.ITEM] dsoTypes: [DSpaceObjectType.ITEM]
})).pipe(toDSpaceObjectListRD()) as Observable<RemoteData<PaginatedList<Item>>>; })).pipe(toDSpaceObjectListRD()) as Observable<RemoteData<PaginatedList<Item>>>;
}), }),
startWith(undefined) // Make sure switching pages shows loading component startWith(undefined) // Make sure switching pages shows loading component
) )
) )
); );

View File

@@ -55,6 +55,7 @@ import { ThemedCommunityPageComponent } from './themed-community-page.component'
id: 'statistics_community_:id', id: 'statistics_community_:id',
active: true, active: true,
visible: true, visible: true,
type: 'statistics',
model: { model: {
type: MenuItemType.LINK, type: MenuItemType.LINK,
text: 'menu.section.statistics', text: 'menu.section.statistics',

View File

@@ -0,0 +1,28 @@
import { Injectable } from '@angular/core';
import { SingleFeatureAuthorizationGuard } from './single-feature-authorization.guard';
import { AuthorizationDataService } from '../authorization-data.service';
import { ActivatedRouteSnapshot, Router, RouterStateSnapshot } from '@angular/router';
import { AuthService } from '../../../auth/auth.service';
import { Observable, of as observableOf } from 'rxjs';
import { FeatureID } from '../feature-id';
import { tap } from 'rxjs/operators';
/**
* Prevent unauthorized activating and loading of routes when the current authenticated user doesn't have group
* management rights
*/
@Injectable({
providedIn: 'root'
})
export class StatisticsAdministratorGuard extends SingleFeatureAuthorizationGuard {
constructor(protected authorizationService: AuthorizationDataService, protected router: Router, protected authService: AuthService) {
super(authorizationService, router, authService);
}
/**
* Check group management rights
*/
getFeatureID(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<FeatureID> {
return observableOf(FeatureID.CanViewUsageStatistics);
}
}

View File

@@ -25,4 +25,5 @@ export enum FeatureID {
CanEditVersion = 'canEditVersion', CanEditVersion = 'canEditVersion',
CanDeleteVersion = 'canDeleteVersion', CanDeleteVersion = 'canDeleteVersion',
CanCreateVersion = 'canCreateVersion', CanCreateVersion = 'canCreateVersion',
CanViewUsageStatistics = 'canViewUsageStatistics',
} }

View File

@@ -21,6 +21,7 @@ import { ThemedHomePageComponent } from './themed-home-page.component';
active: true, active: true,
visible: true, visible: true,
index: 2, index: 2,
type: 'statistics',
model: { model: {
type: MenuItemType.LINK, type: MenuItemType.LINK,
text: 'menu.section.statistics', text: 'menu.section.statistics',

View File

@@ -58,6 +58,7 @@ import { REQUEST_COPY_MODULE_PATH } from '../app-routing-paths';
id: 'statistics_item_:id', id: 'statistics_item_:id',
active: true, active: true,
visible: true, visible: true,
type: 'statistics',
model: { model: {
type: MenuItemType.LINK, type: MenuItemType.LINK,
text: 'menu.section.statistics', text: 'menu.section.statistics',

View File

@@ -1,4 +1,3 @@
<div class="nav-item navbar-section"> <div class="nav-item navbar-section">
<ng-container <ng-container *ngComponentOutlet="(sectionMap$ | async).get(section.id).component; injector: (sectionMap$ | async).get(section.id).injector;"></ng-container>
*ngComponentOutlet="(sectionMap$ | async).get(section.id).component; injector: (sectionMap$ | async).get(section.id).injector;"></ng-container> </div>
</div>

View File

@@ -1,18 +1,15 @@
<nav [ngClass]="{'open': !(menuCollapsed | async)}" <nav [ngClass]="{'open': !(menuCollapsed | async)}" [@slideMobileNav]="!(windowService.isXsOrSm() | async) ? 'default' : ((menuCollapsed | async) ? 'collapsed' : 'expanded')"
[@slideMobileNav]="!(windowService.isXsOrSm() | async) ? 'default' : ((menuCollapsed | async) ? 'collapsed' : 'expanded')" class="navbar navbar-light navbar-expand-md p-md-0 navbar-container" role="navigation" [attr.aria-label]="'nav.main.description' | translate">
class="navbar navbar-light navbar-expand-md p-md-0 navbar-container" <!-- TODO remove navbar-container class when https://github.com/twbs/bootstrap/issues/24726 is fixed -->
role="navigation" [attr.aria-label]="'nav.main.description' | translate"> <!-- TODO remove navbar-container class when https://github.com/twbs/bootstrap/issues/24726 is fixed -->
<div class="container"> <div class="container">
<div class="reset-padding-md w-100"> <div class="reset-padding-md w-100">
<div id="collapsingNav"> <div id="collapsingNav">
<ul class="navbar-nav mr-auto shadow-none"> <ul class="navbar-nav mr-auto shadow-none">
<ng-container *ngFor="let section of (sections | async)"> <ng-container *ngFor="let section of (sections | async)">
<ng-container <ng-container *ngComponentOutlet="(sectionMap$ | async).get(section.id)?.component; injector: (sectionMap$ | async).get(section.id).injector;"></ng-container>
*ngComponentOutlet="(sectionMap$ | async).get(section.id).component; injector: (sectionMap$ | async).get(section.id).injector;"></ng-container>
</ng-container> </ng-container>
</ul> </ul>
</div> </div>
</div> </div>
</div> </div>
</nav> </nav>

View File

@@ -7,6 +7,11 @@ import { TextMenuItemModel } from '../shared/menu/menu-item/models/text.model';
import { LinkMenuItemModel } from '../shared/menu/menu-item/models/link.model'; import { LinkMenuItemModel } from '../shared/menu/menu-item/models/link.model';
import { HostWindowService } from '../shared/host-window.service'; import { HostWindowService } from '../shared/host-window.service';
import { environment } from '../../environments/environment'; import { environment } from '../../environments/environment';
import { AuthorizationDataService } from 'src/app/core/data/feature-authorization/authorization-data.service';
import { Router, ActivatedRoute } from '@angular/router';
import { map, take } from 'rxjs/operators';
import { RemoteData } from '../core/data/remote-data';
import { Collection } from 'src/app/core/shared/collection.model';
/** /**
* Component representing the public navbar * Component representing the public navbar
@@ -25,10 +30,12 @@ export class NavbarComponent extends MenuComponent {
menuID = MenuID.PUBLIC; menuID = MenuID.PUBLIC;
constructor(protected menuService: MenuService, constructor(protected menuService: MenuService,
protected injector: Injector, protected injector: Injector,
public windowService: HostWindowService public windowService: HostWindowService,
public authorizationService: AuthorizationDataService,
public route: ActivatedRoute
) { ) {
super(menuService, injector); super(menuService, injector, authorizationService, route);
} }
ngOnInit(): void { ngOnInit(): void {

View File

@@ -7,9 +7,12 @@ import { MenuComponent } from './menu.component';
import { MenuServiceStub } from '../testing/menu-service.stub'; import { MenuServiceStub } from '../testing/menu-service.stub';
import { of as observableOf } from 'rxjs'; import { of as observableOf } from 'rxjs';
import { MenuSection } from './menu.reducer'; import { MenuSection } from './menu.reducer';
import { Router } from '@angular/router'; import { Router, ActivatedRoute } from '@angular/router';
import { RouterTestingModule } from '@angular/router/testing'; import { RouterTestingModule } from '@angular/router/testing';
import { MenuID } from './initial-menus-state'; import { MenuID } from './initial-menus-state';
import { AuthorizationDataService } from 'src/app/core/data/feature-authorization/authorization-data.service';
import { createSuccessfulRemoteDataObject } from 'src/app/shared/remote-data.utils';
import { Item } from '../../core/shared/item.model';
describe('MenuComponent', () => { describe('MenuComponent', () => {
let comp: MenuComponent; let comp: MenuComponent;
@@ -19,13 +22,38 @@ describe('MenuComponent', () => {
const mockMenuID = 'mock-menuID' as MenuID; const mockMenuID = 'mock-menuID' as MenuID;
const authorizationService = jasmine.createSpyObj('authorizationService', {
isAuthorized: observableOf(true)
});
const mockItem = Object.assign(new Item(), {
id: 'fake-id',
uuid: 'fake-id',
handle: 'fake/handle',
lastModified: '2018',
_links: {
self: {
href: 'https://localhost:8000/items/fake-id'
}
}
});
const routeStub = {
data: observableOf({
dso: createSuccessfulRemoteDataObject(mockItem)
}),
children: []
};
beforeEach(waitForAsync(() => { beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({ TestBed.configureTestingModule({
imports: [TranslateModule.forRoot(), NoopAnimationsModule, RouterTestingModule], imports: [TranslateModule.forRoot(), NoopAnimationsModule, RouterTestingModule],
declarations: [MenuComponent], declarations: [MenuComponent],
providers: [ providers: [
Injector, Injector,
{ provide: MenuService, useClass: MenuServiceStub } { provide: MenuService, useClass: MenuServiceStub },
{ provide: AuthorizationDataService, useValue: authorizationService },
{ provide: ActivatedRoute, useValue: routeStub },
], ],
schemas: [NO_ERRORS_SCHEMA] schemas: [NO_ERRORS_SCHEMA]
}).overrideComponent(MenuComponent, { }).overrideComponent(MenuComponent, {
@@ -95,4 +123,38 @@ describe('MenuComponent', () => {
expect(menuService.collapseMenuPreview).toHaveBeenCalledWith(comp.menuID); expect(menuService.collapseMenuPreview).toHaveBeenCalledWith(comp.menuID);
})); }));
}); });
describe('when unauthorized statistics', () => {
beforeEach(() => {
comp.sections = observableOf([{ 'id': 'browse_global_communities_and_collections', 'active': false, 'visible': true, 'index': 0, 'model': { 'type': 1, 'text': 'menu.section.browse_global_communities_and_collections', 'link': '/community-list' }, 'shouldPersistOnRouteChange': true }, { 'id': 'browse_global', 'active': false, 'visible': true, 'index': 1, 'model': { 'type': 0, 'text': 'menu.section.browse_global' }, 'shouldPersistOnRouteChange': true }, { 'id': 'statistics_site', 'active': true, 'visible': true, 'index': 2, 'type': 'statistics', 'model': { 'type': 1, 'text': 'menu.section.statistics', 'link': 'statistics' } }]);
authorizationService.isAuthorized().and.returnValue(observableOf(false));
fixture.detectChanges();
});
it('when authorized statistics', (done => {
comp.sections.subscribe((sections) => {
expect(sections.length).toEqual(2);
done();
});
}));
});
describe('get authorized statistics', () => {
beforeEach(() => {
comp.sections = observableOf([{ 'id': 'browse_global_communities_and_collections', 'active': false, 'visible': true, 'index': 0, 'model': { 'type': 1, 'text': 'menu.section.browse_global_communities_and_collections', 'link': '/community-list' }, 'shouldPersistOnRouteChange': true }, { 'id': 'browse_global', 'active': false, 'visible': true, 'index': 1, 'model': { 'type': 0, 'text': 'menu.section.browse_global' }, 'shouldPersistOnRouteChange': true }, { 'id': 'statistics_site', 'active': true, 'visible': true, 'index': 2, 'type': 'statistics', 'model': { 'type': 1, 'text': 'menu.section.statistics', 'link': 'statistics' } }]);
fixture.detectChanges();
});
it('get authorized statistics', (done => {
comp.sections.subscribe((sections) => {
expect(sections.length).toEqual(3);
done();
});
}));
});
}); });

View File

@@ -1,14 +1,17 @@
import { ChangeDetectionStrategy, Component, Injector, OnDestroy, OnInit } from '@angular/core'; import { ChangeDetectionStrategy, Component, Injector, OnDestroy, OnInit } from '@angular/core';
import { BehaviorSubject, Observable, Subscription } from 'rxjs'; import { BehaviorSubject, Observable, Subscription, of as observableOf } from 'rxjs';
import { MenuService } from './menu.service'; import { MenuService } from './menu.service';
import { MenuID } from './initial-menus-state'; import { MenuID } from './initial-menus-state';
import { MenuSection } from './menu.reducer'; import { MenuSection } from './menu.reducer';
import { distinctUntilChanged, map, switchMap } from 'rxjs/operators'; import { distinctUntilChanged, map, switchMap, mergeMap, tap, isEmpty } from 'rxjs/operators';
import { GenericConstructor } from '../../core/shared/generic-constructor'; import { GenericConstructor } from '../../core/shared/generic-constructor';
import { hasValue } from '../empty.util'; import { hasValue, isNotEmpty, hasValueOperator, isNotEmptyOperator } from '../empty.util';
import { MenuSectionComponent } from './menu-section/menu-section.component'; import { MenuSectionComponent } from './menu-section/menu-section.component';
import { getComponentForMenu } from './menu-section.decorator'; import { getComponentForMenu } from './menu-section.decorator';
import { compareArraysUsingIds } from '../../item-page/simple/item-types/shared/item-relationships-utils'; import { compareArraysUsingIds } from '../../item-page/simple/item-types/shared/item-relationships-utils';
import { AuthorizationDataService } from 'src/app/core/data/feature-authorization/authorization-data.service';
import { FeatureID } from 'src/app/core/data/feature-authorization/feature-id';
import { Router, ActivatedRoute } from '@angular/router';
/** /**
* A basic implementation of a MenuComponent * A basic implementation of a MenuComponent
@@ -67,27 +70,39 @@ export class MenuComponent implements OnInit, OnDestroy {
*/ */
subs: Subscription[] = []; subs: Subscription[] = [];
constructor(protected menuService: MenuService, protected injector: Injector) { private activatedRouteLastChild: ActivatedRoute;
constructor(protected menuService: MenuService, protected injector: Injector, public authorizationService: AuthorizationDataService, public route: ActivatedRoute) {
} }
/** /**
* Sets all instance variables to their initial values * Sets all instance variables to their initial values
*/ */
ngOnInit(): void { ngOnInit(): void {
this.activatedRouteLastChild = this.getActivatedRoute(this.route);
this.menuCollapsed = this.menuService.isMenuCollapsed(this.menuID); this.menuCollapsed = this.menuService.isMenuCollapsed(this.menuID);
this.menuPreviewCollapsed = this.menuService.isMenuPreviewCollapsed(this.menuID); this.menuPreviewCollapsed = this.menuService.isMenuPreviewCollapsed(this.menuID);
this.menuVisible = this.menuService.isMenuVisible(this.menuID); this.menuVisible = this.menuService.isMenuVisible(this.menuID);
this.sections = this.menuService.getMenuTopSections(this.menuID).pipe(distinctUntilChanged(compareArraysUsingIds())); this.sections = this.menuService.getMenuTopSections(this.menuID).pipe(distinctUntilChanged(compareArraysUsingIds()));
this.subs.push( this.subs.push(
this.sections.pipe( this.sections.pipe(
tap(t => console.log(t)),
// if you return an array from a switchMap it will emit each element as a separate event. // if you return an array from a switchMap it will emit each element as a separate event.
// So this switchMap is equivalent to a subscribe with a forEach inside // So this switchMap is equivalent to a subscribe with a forEach inside
switchMap((sections: MenuSection[]) => sections), switchMap((sections: MenuSection[]) => sections),
switchMap((section: MenuSection) => {
if (section.id.includes('statistics')) {
return this.getAuthorizedStatistics(section);
}
return observableOf(section);
}),
isNotEmptyOperator(),
switchMap((section: MenuSection) => this.getSectionComponent(section).pipe( switchMap((section: MenuSection) => this.getSectionComponent(section).pipe(
map((component: GenericConstructor<MenuSectionComponent>) => ({ section, component })) map((component: GenericConstructor<MenuSectionComponent>) => ({ section, component }))
)), )),
distinctUntilChanged((x,y) => x.section.id === y.section.id) distinctUntilChanged((x, y) => x.section.id === y.section.id)
).subscribe(({ section, component}) => { ).subscribe(({ section, component }) => {
const nextMap = this.sectionMap$.getValue(); const nextMap = this.sectionMap$.getValue();
nextMap.set(section.id, { nextMap.set(section.id, {
injector: this.getSectionDataInjector(section), injector: this.getSectionDataInjector(section),
@@ -98,6 +113,43 @@ export class MenuComponent implements OnInit, OnDestroy {
); );
} }
/**
* Get activated route of the deepest activated route
*/
getActivatedRoute(route) {
if (route.children.length > 0) {
return this.getActivatedRoute(route.firstChild);
} else {
return route;
}
}
/**
* Get section of statistics after checking authorization
*/
getAuthorizedStatistics(section) {
return this.activatedRouteLastChild.data.pipe(
switchMap((data) => {
return this.authorizationService.isAuthorized(FeatureID.CanViewUsageStatistics, this.getObjectUrl(data)).pipe(
map((canViewUsageStatistics: boolean) => {
if (!canViewUsageStatistics) {
return {};
} else {
return section;
}
}));
})
);
}
/**
* Get statistics route dso data
*/
getObjectUrl(data) {
const object = data.site ? data.site : data.dso.payload;
return object._links.self.href;
}
/** /**
* Collapse this menu when it's currently expanded, expand it when its currently collapsed * Collapse this menu when it's currently expanded, expand it when its currently collapsed
* @param {Event} event The user event that triggered this method * @param {Event} event The user event that triggered this method
@@ -164,8 +216,8 @@ export class MenuComponent implements OnInit, OnDestroy {
private getSectionComponent(section: MenuSection): Observable<GenericConstructor<MenuSectionComponent>> { private getSectionComponent(section: MenuSection): Observable<GenericConstructor<MenuSectionComponent>> {
return this.menuService.hasSubSections(this.menuID, section.id).pipe( return this.menuService.hasSubSections(this.menuID, section.id).pipe(
map((expandable: boolean) => { map((expandable: boolean) => {
return getComponentForMenu(this.menuID, expandable); return getComponentForMenu(this.menuID, expandable);
} }
), ),
); );
} }

View File

@@ -19,7 +19,7 @@ export class MenuEffects {
/** /**
* On route change, build menu sections for every menu type depending on the current route data * On route change, build menu sections for every menu type depending on the current route data
*/ */
@Effect({dispatch: false}) @Effect({ dispatch: false })
public buildRouteMenuSections$: Observable<Action> = this.actions$ public buildRouteMenuSections$: Observable<Action> = this.actions$
.pipe( .pipe(
ofType(ROUTER_NAVIGATED), ofType(ROUTER_NAVIGATED),
@@ -31,8 +31,8 @@ export class MenuEffects {
); );
constructor(private actions$: Actions, constructor(private actions$: Actions,
private menuService: MenuService, private menuService: MenuService,
private route: ActivatedRoute) { private route: ActivatedRoute) {
} }
/** /**
@@ -72,7 +72,6 @@ export class MenuEffects {
const last: boolean = hasNoValue(route.firstChild); const last: boolean = hasNoValue(route.firstChild);
if (hasValue(data) && hasValue(data.menu) && hasValue(data.menu[menuID])) { if (hasValue(data) && hasValue(data.menu) && hasValue(data.menu[menuID])) {
let menuSections: MenuSection[] | MenuSection = data.menu[menuID]; let menuSections: MenuSection[] | MenuSection = data.menu[menuID];
menuSections = this.resolveSubstitutions(menuSections, params); menuSections = this.resolveSubstitutions(menuSections, params);
@@ -120,4 +119,5 @@ export class MenuEffects {
} }
return resolved; return resolved;
} }
} }

View File

@@ -10,64 +10,69 @@ import { ThemedCommunityStatisticsPageComponent } from './community-statistics-p
import { ThemedItemStatisticsPageComponent } from './item-statistics-page/themed-item-statistics-page.component'; import { ThemedItemStatisticsPageComponent } from './item-statistics-page/themed-item-statistics-page.component';
import { ThemedSiteStatisticsPageComponent } from './site-statistics-page/themed-site-statistics-page.component'; import { ThemedSiteStatisticsPageComponent } from './site-statistics-page/themed-site-statistics-page.component';
import { ItemResolver } from '../item-page/item.resolver'; import { ItemResolver } from '../item-page/item.resolver';
import { StatisticsAdministratorGuard } from 'src/app/core/data/feature-authorization/feature-authorization-guard/statistics-administrator.guard';
@NgModule({ @NgModule({
imports: [ imports: [
StatisticsPageModule, StatisticsPageModule,
RouterModule.forChild([ RouterModule.forChild([
{ {
path: '', path: '',
resolve: { resolve: {
breadcrumb: I18nBreadcrumbResolver breadcrumb: I18nBreadcrumbResolver
},
data: {
title: 'statistics.title',
breadcrumbKey: 'statistics'
},
children: [
{
path: '',
component: ThemedSiteStatisticsPageComponent,
},
]
}, },
{ data: {
path: `items/:id`, title: 'statistics.title',
resolve: { breadcrumbKey: 'statistics'
scope: ItemResolver,
breadcrumb: I18nBreadcrumbResolver
},
data: {
title: 'statistics.title',
breadcrumbKey: 'statistics'
},
component: ThemedItemStatisticsPageComponent,
}, },
{ children: [
path: `collections/:id`, {
resolve: { path: '',
scope: CollectionPageResolver, component: ThemedSiteStatisticsPageComponent,
breadcrumb: I18nBreadcrumbResolver
}, },
data: { ],
title: 'statistics.title', canActivate: [StatisticsAdministratorGuard]
breadcrumbKey: 'statistics' },
}, {
component: ThemedCollectionStatisticsPageComponent, path: `items/:id`,
resolve: {
scope: ItemResolver,
breadcrumb: I18nBreadcrumbResolver
}, },
{ data: {
path: `communities/:id`, title: 'statistics.title',
resolve: { breadcrumbKey: 'statistics'
scope: CommunityPageResolver,
breadcrumb: I18nBreadcrumbResolver
},
data: {
title: 'statistics.title',
breadcrumbKey: 'statistics'
},
component: ThemedCommunityStatisticsPageComponent,
}, },
] component: ThemedItemStatisticsPageComponent,
canActivate: [StatisticsAdministratorGuard]
},
{
path: `collections/:id`,
resolve: {
scope: CollectionPageResolver,
breadcrumb: I18nBreadcrumbResolver
},
data: {
title: 'statistics.title',
breadcrumbKey: 'statistics'
},
component: ThemedCollectionStatisticsPageComponent,
canActivate: [StatisticsAdministratorGuard]
},
{
path: `communities/:id`,
resolve: {
scope: CommunityPageResolver,
breadcrumb: I18nBreadcrumbResolver
},
data: {
title: 'statistics.title',
breadcrumbKey: 'statistics'
},
component: ThemedCommunityStatisticsPageComponent,
canActivate: [StatisticsAdministratorGuard]
},
]
) )
], ],
providers: [ providers: [

View File

@@ -15,6 +15,6 @@ getTestBed().initTestEnvironment(
platformBrowserDynamicTesting() platformBrowserDynamicTesting()
); );
// Then we find all the tests. // Then we find all the tests.
const context = require.context('./', true, /\.spec\.ts$/); const context = require.context('./app/shared/menu', true, /\.spec\.ts$/);
// And load the modules. // And load the modules.
context.keys().map(context); context.keys().map(context);

View File

@@ -1,17 +1,14 @@
<nav [ngClass]="{'open': !(menuCollapsed | async)}" <nav [ngClass]="{'open': !(menuCollapsed | async)}" [@slideMobileNav]="!(windowService.isXsOrSm() | async) ? 'default' : ((menuCollapsed | async) ? 'collapsed' : 'expanded')"
[@slideMobileNav]="!(windowService.isXsOrSm() | async) ? 'default' : ((menuCollapsed | async) ? 'collapsed' : 'expanded')" class="navbar navbar-expand-md navbar-light p-0 navbar-container" role="navigation" [attr.aria-label]="'nav.main.description' | translate">
class="navbar navbar-expand-md navbar-light p-0 navbar-container"
role="navigation" [attr.aria-label]="'nav.main.description' | translate">
<div class="container h-100"> <div class="container h-100">
<a class="navbar-brand my-2" routerLink="/home"> <a class="navbar-brand my-2" routerLink="/home">
<img src="assets/images/dspace-logo.svg" [attr.alt]="'menu.header.image.logo' | translate"/> <img src="assets/images/dspace-logo.svg" [attr.alt]="'menu.header.image.logo' | translate" />
</a> </a>
<div id="collapsingNav" class="w-100 h-100"> <div id="collapsingNav" class="w-100 h-100">
<ul class="navbar-nav me-auto mb-2 mb-lg-0 h-100"> <ul class="navbar-nav me-auto mb-2 mb-lg-0 h-100">
<ng-container *ngFor="let section of (sections | async)"> <ng-container *ngFor="let section of (sections | async)">
<ng-container <ng-container *ngComponentOutlet="(sectionMap$ | async).get(section.id)?.component; injector: (sectionMap$ | async).get(section.id)?.injector;"></ng-container>
*ngComponentOutlet="(sectionMap$ | async).get(section.id).component; injector: (sectionMap$ | async).get(section.id).injector;"></ng-container>
</ng-container> </ng-container>
</ul> </ul>
</div> </div>
@@ -20,5 +17,4 @@
<ds-auth-nav-menu class="navbar-collapsed"></ds-auth-nav-menu> <ds-auth-nav-menu class="navbar-collapsed"></ds-auth-nav-menu>
<ds-impersonate-navbar class="navbar-collapsed"></ds-impersonate-navbar> <ds-impersonate-navbar class="navbar-collapsed"></ds-impersonate-navbar>
</div> </div>
</nav> </nav>