mirror of
https://github.com/DSpace/dspace-angular.git
synced 2025-10-07 10:04:11 +00:00
Merge pull request #3994 from atmire/refactor-menu-resolvers-9.0
Refactor menu resolvers 9.0
This commit is contained in:
@@ -7,7 +7,7 @@ describe('Item Statistics Page', () => {
|
||||
it('should load if you click on "Statistics" from an Item/Entity page', () => {
|
||||
cy.visit('/entities/publication/'.concat(Cypress.env('DSPACE_TEST_ENTITY_PUBLICATION')));
|
||||
cy.get('ds-navbar ds-link-menu-item a[data-test="link-menu-item.menu.section.statistics"]').click();
|
||||
cy.location('pathname').should('eq', ITEMSTATISTICSPAGE);
|
||||
cy.location('pathname').should('eq', '/statistics/entities/publication/'.concat(Cypress.env('DSPACE_TEST_ENTITY_PUBLICATION')));
|
||||
});
|
||||
|
||||
it('should contain element ds-item-statistics-page when navigating to an item statistics page', () => {
|
||||
|
@@ -3,7 +3,7 @@
|
||||
[ngClass]="{ disabled: isDisabled }"
|
||||
role="menuitem"
|
||||
[attr.aria-disabled]="isDisabled"
|
||||
[attr.aria-labelledby]="adminMenuSectionTitleId(section.id)"
|
||||
[attr.aria-labelledby]="adminMenuSectionTitleAccessibilityHandle(section)"
|
||||
[routerLink]="itemModel.link"
|
||||
(keyup.space)="navigate($event)"
|
||||
(keyup.enter)="navigate($event)"
|
||||
@@ -14,7 +14,7 @@
|
||||
</div>
|
||||
<div class="sidebar-collapsible-element-outer-wrapper">
|
||||
<div class="sidebar-collapsible-element-inner-wrapper sidebar-item">
|
||||
<span [id]="adminMenuSectionTitleId(section.id)" [attr.data-test]="adminMenuSectionTitleId(section.id) | dsBrowserOnly">
|
||||
<span [id]="adminMenuSectionTitleAccessibilityHandle(section)" [attr.data-test]="adminMenuSectionTitleAccessibilityHandle(section) | dsBrowserOnly">
|
||||
{{itemModel.text | translate}}
|
||||
</span>
|
||||
</div>
|
||||
|
@@ -16,7 +16,7 @@ import { MenuService } from '../../../shared/menu/menu.service';
|
||||
import { MenuID } from '../../../shared/menu/menu-id.model';
|
||||
import { LinkMenuItemModel } from '../../../shared/menu/menu-item/models/link.model';
|
||||
import { MenuSection } from '../../../shared/menu/menu-section.model';
|
||||
import { MenuSectionComponent } from '../../../shared/menu/menu-section/menu-section.component';
|
||||
import { AbstractMenuSectionComponent } from '../../../shared/menu/menu-section/abstract-menu-section.component';
|
||||
import { BrowserOnlyPipe } from '../../../shared/utils/browser-only.pipe';
|
||||
|
||||
/**
|
||||
@@ -30,7 +30,7 @@ import { BrowserOnlyPipe } from '../../../shared/utils/browser-only.pipe';
|
||||
imports: [NgClass, RouterLink, TranslateModule, BrowserOnlyPipe],
|
||||
|
||||
})
|
||||
export class AdminSidebarSectionComponent extends MenuSectionComponent implements OnInit {
|
||||
export class AdminSidebarSectionComponent extends AbstractMenuSectionComponent implements OnInit {
|
||||
|
||||
/**
|
||||
* This section resides in the Admin Sidebar
|
||||
@@ -44,16 +44,17 @@ export class AdminSidebarSectionComponent extends MenuSectionComponent implement
|
||||
isDisabled: boolean;
|
||||
|
||||
constructor(
|
||||
@Inject('sectionDataProvider') menuSection: MenuSection,
|
||||
@Inject('sectionDataProvider') protected section: MenuSection,
|
||||
protected menuService: MenuService,
|
||||
protected injector: Injector,
|
||||
protected router: Router,
|
||||
) {
|
||||
super(menuSection, menuService, injector);
|
||||
this.itemModel = menuSection.model as LinkMenuItemModel;
|
||||
super(menuService, injector);
|
||||
this.itemModel = section.model as LinkMenuItemModel;
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
// todo: should support all menu entries?
|
||||
this.isDisabled = this.itemModel?.disabled || isEmpty(this.itemModel?.link);
|
||||
super.ngOnInit();
|
||||
}
|
||||
@@ -65,11 +66,13 @@ export class AdminSidebarSectionComponent extends MenuSectionComponent implement
|
||||
}
|
||||
}
|
||||
|
||||
adminMenuSectionId(sectionId: string) {
|
||||
return `admin-menu-section-${sectionId}`;
|
||||
adminMenuSectionId(section: MenuSection) {
|
||||
const accessibilityHandle = section.accessibilityHandle ?? section.id;
|
||||
return `admin-menu-section-${accessibilityHandle}`;
|
||||
}
|
||||
|
||||
adminMenuSectionTitleId(sectionId: string) {
|
||||
return `admin-menu-section-${sectionId}-title`;
|
||||
adminMenuSectionTitleAccessibilityHandle(section: MenuSection) {
|
||||
const accessibilityHandle = section.accessibilityHandle ?? section.id;
|
||||
return `admin-menu-section-${accessibilityHandle}-title`;
|
||||
}
|
||||
}
|
||||
|
@@ -1,4 +1,6 @@
|
||||
<div [ngClass]="{'expanded': (isExpanded$ | async)}"
|
||||
@if (hasSubSections$ | async) {
|
||||
<div
|
||||
[ngClass]="{'expanded': (isExpanded$ | async)}"
|
||||
[@bgColor]="{
|
||||
value: ((isExpanded$ | async) ? 'endBackground' : 'startBackground'),
|
||||
params: {endColor: (sidebarActiveBg$ | async)}
|
||||
@@ -6,7 +8,7 @@
|
||||
<a class="sidebar-section-wrapper"
|
||||
role="menuitem" tabindex="0"
|
||||
aria-haspopup="menu"
|
||||
[attr.aria-controls]="adminMenuSectionId(section.id)"
|
||||
[attr.aria-controls]="adminMenuSectionId(section)"
|
||||
[attr.aria-expanded]="isExpanded$ | async"
|
||||
[attr.aria-label]="('menu.section.toggle.' + section.id) | translate"
|
||||
[class.disabled]="section.model?.disabled"
|
||||
@@ -15,11 +17,11 @@
|
||||
href="javascript:void(0);"
|
||||
>
|
||||
<div class="sidebar-fixed-element-wrapper" data-test="sidebar-section-icon" aria-hidden="true">
|
||||
<i class="fas fa-{{section.icon}} fa-fw"></i>
|
||||
<i class="fas fa-{{section.icon ?? 'notdef'}} fa-fw"></i>
|
||||
</div>
|
||||
<div class="sidebar-collapsible-element-outer-wrapper">
|
||||
<div class="sidebar-collapsible-element-inner-wrapper sidebar-item toggler-wrapper">
|
||||
<span [id]="adminMenuSectionTitleId(section.id)" [attr.data-test]="adminMenuSectionTitleId(section.id) | dsBrowserOnly">
|
||||
<span [id]="adminMenuSectionTitleAccessibilityHandle(section)" [attr.data-test]="adminMenuSectionTitleAccessibilityHandle(section) | dsBrowserOnly">
|
||||
<ng-container
|
||||
*ngComponentOutlet="(sectionMap$ | async).get(section.id).component; injector: (sectionMap$ | async).get(section.id).injector;"></ng-container>
|
||||
</span>
|
||||
@@ -34,7 +36,7 @@
|
||||
<div class="sidebar-fixed-element-wrapper"></div>
|
||||
<div class="sidebar-collapsible-element-outer-wrapper">
|
||||
<div class="sidebar-collapsible-element-inner-wrapper">
|
||||
<div class="sidebar-sub-level-item-list" role="menu" [id]="adminMenuSectionId(section.id)" [attr.aria-label]="('menu.section.' + section.id) | translate">
|
||||
<div class="sidebar-sub-level-item-list" role="menu" [id]="adminMenuSectionId(section)" [attr.aria-label]="('menu.section.' + section.id) | translate">
|
||||
@for (subSection of (subSections$ | async); track subSection) {
|
||||
<div class="sidebar-item">
|
||||
<ng-container
|
||||
@@ -47,3 +49,4 @@
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
@@ -11,6 +11,7 @@ import { TranslateModule } from '@ngx-translate/core';
|
||||
import { of as observableOf } from 'rxjs';
|
||||
|
||||
import { MenuService } from '../../../shared/menu/menu.service';
|
||||
import { MenuItemModels } from '../../../shared/menu/menu-section.model';
|
||||
import { CSSVariableService } from '../../../shared/sass-helper/css-variable.service';
|
||||
import { CSSVariableServiceStub } from '../../../shared/testing/css-variable-service.stub';
|
||||
import { MenuServiceStub } from '../../../shared/testing/menu-service.stub';
|
||||
@@ -22,6 +23,9 @@ describe('ExpandableAdminSidebarSectionComponent', () => {
|
||||
let fixture: ComponentFixture<ExpandableAdminSidebarSectionComponent>;
|
||||
const menuService = new MenuServiceStub();
|
||||
const iconString = 'test';
|
||||
|
||||
|
||||
describe('when there are subsections', () => {
|
||||
beforeEach(waitForAsync(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [NoopAnimationsModule, TranslateModule.forRoot(), ExpandableAdminSidebarSectionComponent, TestComponent],
|
||||
@@ -35,7 +39,11 @@ describe('ExpandableAdminSidebarSectionComponent', () => {
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
spyOn(menuService, 'getSubSectionsByParentID').and.returnValue(observableOf([]));
|
||||
spyOn(menuService, 'getSubSectionsByParentID').and.returnValue(observableOf([{
|
||||
id: 'test',
|
||||
visible: true,
|
||||
model: {} as MenuItemModels,
|
||||
}]));
|
||||
fixture = TestBed.createComponent(ExpandableAdminSidebarSectionComponent);
|
||||
component = fixture.componentInstance;
|
||||
spyOn(component as any, 'getMenuItemComponent').and.returnValue(TestComponent);
|
||||
@@ -67,6 +75,41 @@ describe('ExpandableAdminSidebarSectionComponent', () => {
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
describe('when there are no subsections', () => {
|
||||
beforeEach(waitForAsync(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [NoopAnimationsModule, TranslateModule.forRoot(), ExpandableAdminSidebarSectionComponent, TestComponent],
|
||||
providers: [
|
||||
{ provide: 'sectionDataProvider', useValue: { icon: iconString, model: {} } },
|
||||
{ provide: MenuService, useValue: menuService },
|
||||
{ provide: CSSVariableService, useClass: CSSVariableServiceStub },
|
||||
{ provide: Router, useValue: new RouterStub() },
|
||||
],
|
||||
}).compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
spyOn(menuService, 'getSubSectionsByParentID').and.returnValue(observableOf([]));
|
||||
fixture = TestBed.createComponent(ExpandableAdminSidebarSectionComponent);
|
||||
component = fixture.componentInstance;
|
||||
spyOn(component as any, 'getMenuItemComponent').and.returnValue(TestComponent);
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should not contain a section', () => {
|
||||
const icon = fixture.debugElement.query(By.css('.shortcut-icon'));
|
||||
expect(icon).toBeNull();
|
||||
const sidebarToggler = fixture.debugElement.query(By.css('.sidebar-section'));
|
||||
expect(sidebarToggler).toBeNull();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// declare a test component
|
||||
@Component({
|
||||
selector: 'ds-test-cmp',
|
||||
|
@@ -20,8 +20,10 @@ import { map } from 'rxjs/operators';
|
||||
import { bgColor } from '../../../shared/animations/bgColor';
|
||||
import { rotate } from '../../../shared/animations/rotate';
|
||||
import { slide } from '../../../shared/animations/slide';
|
||||
import { isNotEmpty } from '../../../shared/empty.util';
|
||||
import { MenuService } from '../../../shared/menu/menu.service';
|
||||
import { MenuID } from '../../../shared/menu/menu-id.model';
|
||||
import { MenuSection } from '../../../shared/menu/menu-section.model';
|
||||
import { CSSVariableService } from '../../../shared/sass-helper/css-variable.service';
|
||||
import { BrowserOnlyPipe } from '../../../shared/utils/browser-only.pipe';
|
||||
import { AdminSidebarSectionComponent } from '../admin-sidebar-section/admin-sidebar-section.component';
|
||||
@@ -65,14 +67,20 @@ export class ExpandableAdminSidebarSectionComponent extends AdminSidebarSectionC
|
||||
*/
|
||||
isExpanded$: Observable<boolean>;
|
||||
|
||||
/**
|
||||
* Emits true when the top section has subsections, else emits false
|
||||
*/
|
||||
hasSubSections$: Observable<boolean>;
|
||||
|
||||
|
||||
constructor(
|
||||
@Inject('sectionDataProvider') menuSection,
|
||||
@Inject('sectionDataProvider') protected section: MenuSection,
|
||||
protected menuService: MenuService,
|
||||
private variableService: CSSVariableService,
|
||||
protected injector: Injector,
|
||||
protected router: Router,
|
||||
) {
|
||||
super(menuSection, menuService, injector, router);
|
||||
super(section, menuService, injector, router);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -80,6 +88,9 @@ export class ExpandableAdminSidebarSectionComponent extends AdminSidebarSectionC
|
||||
*/
|
||||
ngOnInit(): void {
|
||||
super.ngOnInit();
|
||||
this.hasSubSections$ = this.subSections$.pipe(
|
||||
map((subSections) => isNotEmpty(subSections)),
|
||||
);
|
||||
this.sidebarActiveBg$ = this.variableService.getVariable('--ds-admin-sidebar-active-bg');
|
||||
this.isSidebarCollapsed$ = this.menuService.isMenuCollapsed(this.menuID);
|
||||
this.isSidebarPreviewCollapsed$ = this.menuService.isMenuPreviewCollapsed(this.menuID);
|
||||
|
@@ -34,7 +34,6 @@ import { forgotPasswordCheckGuard } from './core/rest-property/forgot-password-c
|
||||
import { ServerCheckGuard } from './core/server-check/server-check.guard';
|
||||
import { ThemedForbiddenComponent } from './forbidden/themed-forbidden.component';
|
||||
import { ITEM_MODULE_PATH } from './item-page/item-page-routing-paths';
|
||||
import { menuResolver } from './menuResolver';
|
||||
import { provideSuggestionNotificationsState } from './notifications/provide-suggestion-notifications-state';
|
||||
import { ThemedPageErrorComponent } from './page-error/themed-page-error.component';
|
||||
import { ThemedPageInternalServerErrorComponent } from './page-internal-server-error/themed-page-internal-server-error.component';
|
||||
@@ -50,7 +49,6 @@ export const APP_ROUTES: Route[] = [
|
||||
path: '',
|
||||
canActivate: [authBlockingGuard],
|
||||
canActivateChild: [ServerCheckGuard],
|
||||
resolve: [menuResolver],
|
||||
children: [
|
||||
{ path: '', redirectTo: '/home', pathMatch: 'full' },
|
||||
{
|
||||
|
@@ -39,6 +39,7 @@ import { StoreDevModules } from '../config/store/devtools';
|
||||
import { environment } from '../environments/environment';
|
||||
import { EagerThemesModule } from '../themes/eager-themes.module';
|
||||
import { appEffects } from './app.effects';
|
||||
import { MENUS } from './app.menus';
|
||||
import {
|
||||
appMetaReducers,
|
||||
debugMetaReducers,
|
||||
@@ -159,6 +160,10 @@ export const commonAppConfig: ApplicationConfig = {
|
||||
},
|
||||
// register the dynamic matcher used by form. MUST be provided by the app module
|
||||
...DYNAMIC_MATCHER_PROVIDERS,
|
||||
|
||||
// DI-composable menus
|
||||
...MENUS,
|
||||
|
||||
provideCore(),
|
||||
],
|
||||
};
|
||||
|
101
src/app/app.menus.ts
Normal file
101
src/app/app.menus.ts
Normal file
@@ -0,0 +1,101 @@
|
||||
/**
|
||||
* The contents of this file are subject to the license and copyright
|
||||
* detailed in the LICENSE and NOTICE files at the root of the source
|
||||
* tree and available online at
|
||||
*
|
||||
* http://www.dspace.org/license/
|
||||
*/
|
||||
import { buildMenuStructure } from './shared/menu/menu.structure';
|
||||
import { MenuID } from './shared/menu/menu-id.model';
|
||||
import { MenuRoute } from './shared/menu/menu-route.model';
|
||||
import { AccessControlMenuProvider } from './shared/menu/providers/access-control.menu';
|
||||
import { AdminSearchMenuProvider } from './shared/menu/providers/admin-search.menu';
|
||||
import { BrowseMenuProvider } from './shared/menu/providers/browse.menu';
|
||||
import { CoarNotifyMenuProvider } from './shared/menu/providers/coar-notify.menu';
|
||||
import { SubscribeMenuProvider } from './shared/menu/providers/comcol-subscribe.menu';
|
||||
import { CommunityListMenuProvider } from './shared/menu/providers/community-list.menu';
|
||||
import { CreateReportMenuProvider } from './shared/menu/providers/create-report.menu';
|
||||
import { CurationMenuProvider } from './shared/menu/providers/curation.menu';
|
||||
import { DSpaceObjectEditMenuProvider } from './shared/menu/providers/dso-edit.menu';
|
||||
import { DsoOptionMenuProvider } from './shared/menu/providers/dso-option.menu';
|
||||
import { EditMenuProvider } from './shared/menu/providers/edit.menu';
|
||||
import { ExportMenuProvider } from './shared/menu/providers/export.menu';
|
||||
import { HealthMenuProvider } from './shared/menu/providers/health.menu';
|
||||
import { ImportMenuProvider } from './shared/menu/providers/import.menu';
|
||||
import { ClaimMenuProvider } from './shared/menu/providers/item-claim.menu';
|
||||
import { OrcidMenuProvider } from './shared/menu/providers/item-orcid.menu';
|
||||
import { VersioningMenuProvider } from './shared/menu/providers/item-versioning.menu';
|
||||
import { NewMenuProvider } from './shared/menu/providers/new.menu';
|
||||
import { NotificationsMenuProvider } from './shared/menu/providers/notifications.menu';
|
||||
import { ProcessesMenuProvider } from './shared/menu/providers/processes.menu';
|
||||
import { RegistriesMenuProvider } from './shared/menu/providers/registries.menu';
|
||||
import { StatisticsMenuProvider } from './shared/menu/providers/statistics.menu';
|
||||
import { SystemWideAlertMenuProvider } from './shared/menu/providers/system-wide-alert.menu';
|
||||
import { WithdrawnReinstateItemMenuProvider } from './shared/menu/providers/withdrawn-reinstate-item.menu';
|
||||
import { WorkflowMenuProvider } from './shared/menu/providers/workflow.menu';
|
||||
|
||||
/**
|
||||
* Represents and builds the menu structure for the three available menus (public navbar, admin sidebar and the dso edit
|
||||
* menus).
|
||||
* The structure consists of a list of menu IDs with each of them having a list of providers that will create the
|
||||
* sections to be part of the menu matching the ID.
|
||||
*
|
||||
* The following menu groups are present in this structure:
|
||||
* - `MenuID.PUBLIC`: Defines menus accessible by the public in the navigation bar.
|
||||
* - `MenuID.ADMIN`: Defines menus for administrative users in the sidebar.
|
||||
* - `MenuID.DSO_EDIT`: Defines dynamic menu options for DSpace Objects that will be present on the DSpace Object's page.
|
||||
*
|
||||
* To add more menu sections to a menu (public navbar, admin sidebar or the dso edit menus),
|
||||
* a new provider can be added to the list with the corresponding menu ID.
|
||||
*
|
||||
* The configuration supports route-specific menu providers and hierarchically structured menu options.
|
||||
*/
|
||||
export const MENUS = buildMenuStructure({
|
||||
[MenuID.PUBLIC]: [
|
||||
CommunityListMenuProvider,
|
||||
BrowseMenuProvider,
|
||||
StatisticsMenuProvider,
|
||||
],
|
||||
[MenuID.ADMIN]: [
|
||||
NewMenuProvider,
|
||||
EditMenuProvider,
|
||||
ImportMenuProvider,
|
||||
ExportMenuProvider,
|
||||
NotificationsMenuProvider,
|
||||
AccessControlMenuProvider,
|
||||
AdminSearchMenuProvider,
|
||||
CreateReportMenuProvider,
|
||||
RegistriesMenuProvider,
|
||||
CurationMenuProvider,
|
||||
ProcessesMenuProvider,
|
||||
WorkflowMenuProvider,
|
||||
HealthMenuProvider,
|
||||
SystemWideAlertMenuProvider,
|
||||
CoarNotifyMenuProvider,
|
||||
],
|
||||
[MenuID.DSO_EDIT]: [
|
||||
DsoOptionMenuProvider.withSubs([
|
||||
SubscribeMenuProvider.onRoute(
|
||||
MenuRoute.COMMUNITY_PAGE,
|
||||
MenuRoute.COLLECTION_PAGE,
|
||||
),
|
||||
DSpaceObjectEditMenuProvider.onRoute(
|
||||
MenuRoute.COMMUNITY_PAGE,
|
||||
MenuRoute.COLLECTION_PAGE,
|
||||
MenuRoute.ITEM_PAGE,
|
||||
),
|
||||
WithdrawnReinstateItemMenuProvider.onRoute(
|
||||
MenuRoute.ITEM_PAGE,
|
||||
),
|
||||
VersioningMenuProvider.onRoute(
|
||||
MenuRoute.ITEM_PAGE,
|
||||
),
|
||||
OrcidMenuProvider.onRoute(
|
||||
MenuRoute.ITEM_PAGE,
|
||||
),
|
||||
ClaimMenuProvider.onRoute(
|
||||
MenuRoute.ITEM_PAGE,
|
||||
),
|
||||
]),
|
||||
],
|
||||
});
|
@@ -8,9 +8,7 @@ import { communityBreadcrumbResolver } from '../core/breadcrumbs/community-bread
|
||||
import { i18nBreadcrumbResolver } from '../core/breadcrumbs/i18n-breadcrumb.resolver';
|
||||
import { ComcolBrowseByComponent } from '../shared/comcol/sections/comcol-browse-by/comcol-browse-by.component';
|
||||
import { ComcolSearchSectionComponent } from '../shared/comcol/sections/comcol-search-section/comcol-search-section.component';
|
||||
import { dsoEditMenuResolver } from '../shared/dso-page/dso-edit-menu.resolver';
|
||||
import { LinkMenuItemModel } from '../shared/menu/menu-item/models/link.model';
|
||||
import { MenuItemType } from '../shared/menu/menu-item-type.model';
|
||||
import { MenuRoute } from '../shared/menu/menu-route.model';
|
||||
import { collectionPageResolver } from './collection-page.resolver';
|
||||
import { collectionPageAdministratorGuard } from './collection-page-administrator.guard';
|
||||
import {
|
||||
@@ -82,8 +80,8 @@ export const ROUTES: Route[] = [
|
||||
{
|
||||
path: '',
|
||||
component: ThemedCollectionPageComponent,
|
||||
resolve: {
|
||||
menu: dsoEditMenuResolver,
|
||||
data: {
|
||||
menuRoute: MenuRoute.COLLECTION_PAGE,
|
||||
},
|
||||
children: [
|
||||
{
|
||||
@@ -108,25 +106,13 @@ export const ROUTES: Route[] = [
|
||||
resolve: {
|
||||
breadcrumb: browseByI18nBreadcrumbResolver,
|
||||
},
|
||||
data: { breadcrumbKey: 'browse.metadata' },
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
data: {
|
||||
menu: {
|
||||
public: [{
|
||||
id: 'statistics_collection_:id',
|
||||
active: true,
|
||||
visible: true,
|
||||
index: 2,
|
||||
model: {
|
||||
type: MenuItemType.LINK,
|
||||
text: 'menu.section.statistics',
|
||||
link: 'statistics/collections/:id/',
|
||||
} as LinkMenuItemModel,
|
||||
}],
|
||||
breadcrumbKey: 'browse.metadata',
|
||||
menuRoute: MenuRoute.COLLECTION_PAGE,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
@@ -7,9 +7,7 @@ import { communityBreadcrumbResolver } from '../core/breadcrumbs/community-bread
|
||||
import { i18nBreadcrumbResolver } from '../core/breadcrumbs/i18n-breadcrumb.resolver';
|
||||
import { ComcolBrowseByComponent } from '../shared/comcol/sections/comcol-browse-by/comcol-browse-by.component';
|
||||
import { ComcolSearchSectionComponent } from '../shared/comcol/sections/comcol-search-section/comcol-search-section.component';
|
||||
import { dsoEditMenuResolver } from '../shared/dso-page/dso-edit-menu.resolver';
|
||||
import { LinkMenuItemModel } from '../shared/menu/menu-item/models/link.model';
|
||||
import { MenuItemType } from '../shared/menu/menu-item-type.model';
|
||||
import { MenuRoute } from '../shared/menu/menu-route.model';
|
||||
import { communityPageResolver } from './community-page.resolver';
|
||||
import { communityPageAdministratorGuard } from './community-page-administrator.guard';
|
||||
import {
|
||||
@@ -69,8 +67,8 @@ export const ROUTES: Route[] = [
|
||||
{
|
||||
path: '',
|
||||
component: ThemedCommunityPageComponent,
|
||||
resolve: {
|
||||
menu: dsoEditMenuResolver,
|
||||
data: {
|
||||
menuRoute: MenuRoute.COMMUNITY_PAGE,
|
||||
},
|
||||
children: [
|
||||
{
|
||||
@@ -94,7 +92,10 @@ export const ROUTES: Route[] = [
|
||||
resolve: {
|
||||
breadcrumb: i18nBreadcrumbResolver,
|
||||
},
|
||||
data: { breadcrumbKey: 'community.subcoms-cols' },
|
||||
data: {
|
||||
breadcrumbKey: 'community.subcoms-cols',
|
||||
menuRoute: MenuRoute.COMMUNITY_PAGE,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'browse/:id',
|
||||
@@ -104,25 +105,13 @@ export const ROUTES: Route[] = [
|
||||
resolve: {
|
||||
breadcrumb: browseByI18nBreadcrumbResolver,
|
||||
},
|
||||
data: { breadcrumbKey: 'browse.metadata' },
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
data: {
|
||||
menu: {
|
||||
public: [{
|
||||
id: 'statistics_community_:id',
|
||||
active: true,
|
||||
visible: true,
|
||||
index: 2,
|
||||
model: {
|
||||
type: MenuItemType.LINK,
|
||||
text: 'menu.section.statistics',
|
||||
link: 'statistics/communities/:id/',
|
||||
} as LinkMenuItemModel,
|
||||
}],
|
||||
breadcrumbKey: 'browse.metadata',
|
||||
menuRoute: MenuRoute.COMMUNITY_PAGE,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
@@ -42,10 +42,10 @@ import { HeadTagService } from './core/metadata/head-tag.service';
|
||||
import { CorrelationIdService } from './correlation-id/correlation-id.service';
|
||||
import { dsDynamicFormControlMapFn } from './shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form-control-map-fn';
|
||||
import { MenuService } from './shared/menu/menu.service';
|
||||
import { MenuProviderService } from './shared/menu/menu-provider.service';
|
||||
import { ThemeService } from './shared/theme-support/theme.service';
|
||||
import { Angulartics2DSpace } from './statistics/angulartics/dspace-provider';
|
||||
|
||||
|
||||
/**
|
||||
* Performs the initialization of the app.
|
||||
*
|
||||
@@ -74,6 +74,7 @@ export abstract class InitService {
|
||||
protected breadcrumbsService: BreadcrumbsService,
|
||||
protected themeService: ThemeService,
|
||||
protected menuService: MenuService,
|
||||
protected menuProviderService: MenuProviderService,
|
||||
|
||||
) {
|
||||
}
|
||||
@@ -216,7 +217,6 @@ export abstract class InitService {
|
||||
this.headTagService.listenForRouteChange();
|
||||
this.breadcrumbsService.listenForRouteChanges();
|
||||
this.themeService.listenForRouteChanges();
|
||||
this.menuService.listenForRouteChanges();
|
||||
}
|
||||
|
||||
/**
|
||||
|
@@ -4,9 +4,7 @@ import { REQUEST_COPY_MODULE_PATH } from '../app-routing-paths';
|
||||
import { accessTokenResolver } from '../core/auth/access-token.resolver';
|
||||
import { authenticatedGuard } from '../core/auth/authenticated.guard';
|
||||
import { itemBreadcrumbResolver } from '../core/breadcrumbs/item-breadcrumb.resolver';
|
||||
import { dsoEditMenuResolver } from '../shared/dso-page/dso-edit-menu.resolver';
|
||||
import { LinkMenuItemModel } from '../shared/menu/menu-item/models/link.model';
|
||||
import { MenuItemType } from '../shared/menu/menu-item-type.model';
|
||||
import { MenuRoute } from '../shared/menu/menu-route.model';
|
||||
import { BitstreamRequestACopyPageComponent } from './bitstreams/request-a-copy/bitstream-request-a-copy-page.component';
|
||||
import { UploadBitstreamComponent } from './bitstreams/upload/upload-bitstream.component';
|
||||
import { ThemedFullItemPageComponent } from './full/themed-full-item-page.component';
|
||||
@@ -37,16 +35,18 @@ export const ROUTES: Route[] = [
|
||||
path: '',
|
||||
component: ThemedItemPageComponent,
|
||||
pathMatch: 'full',
|
||||
resolve: {
|
||||
menu: dsoEditMenuResolver,
|
||||
data: {
|
||||
menuRoute: MenuRoute.ITEM_PAGE,
|
||||
},
|
||||
|
||||
},
|
||||
{
|
||||
path: 'full',
|
||||
component: ThemedFullItemPageComponent,
|
||||
resolve: {
|
||||
menu: dsoEditMenuResolver,
|
||||
data: {
|
||||
menuRoute: MenuRoute.ITEM_PAGE,
|
||||
},
|
||||
|
||||
},
|
||||
{
|
||||
path: ITEM_EDIT_PATH,
|
||||
@@ -75,21 +75,6 @@ export const ROUTES: Route[] = [
|
||||
},
|
||||
},
|
||||
],
|
||||
data: {
|
||||
menu: {
|
||||
public: [{
|
||||
id: 'statistics_item_:id',
|
||||
active: true,
|
||||
visible: true,
|
||||
index: 2,
|
||||
model: {
|
||||
type: MenuItemType.LINK,
|
||||
text: 'menu.section.statistics',
|
||||
link: 'statistics/items/:id/',
|
||||
} as LinkMenuItemModel,
|
||||
}],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'version',
|
||||
|
@@ -8,6 +8,8 @@ export function getItemModuleRoute() {
|
||||
return `/${ITEM_MODULE_PATH}`;
|
||||
}
|
||||
|
||||
export const ENTITY_MODULE_PATH = 'entities';
|
||||
|
||||
/**
|
||||
* Get the route to an item's page
|
||||
* Depending on the item's entity type, the route will either start with /items or /entities
|
||||
|
@@ -1,879 +0,0 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import {
|
||||
ActivatedRouteSnapshot,
|
||||
RouterStateSnapshot,
|
||||
} from '@angular/router';
|
||||
import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
|
||||
import {
|
||||
combineLatest,
|
||||
combineLatest as observableCombineLatest,
|
||||
mergeMap,
|
||||
Observable,
|
||||
of as observableOf,
|
||||
} from 'rxjs';
|
||||
import {
|
||||
filter,
|
||||
find,
|
||||
map,
|
||||
take,
|
||||
} from 'rxjs/operators';
|
||||
|
||||
import { environment } from '../environments/environment';
|
||||
import { PUBLICATION_CLAIMS_PATH } from './admin/admin-notifications/admin-notifications-routing-paths';
|
||||
import { AuthService } from './core/auth/auth.service';
|
||||
import { BrowseService } from './core/browse/browse.service';
|
||||
import { ConfigurationDataService } from './core/data/configuration-data.service';
|
||||
import { AuthorizationDataService } from './core/data/feature-authorization/authorization-data.service';
|
||||
import { FeatureID } from './core/data/feature-authorization/feature-id';
|
||||
import { PaginatedList } from './core/data/paginated-list.model';
|
||||
import {
|
||||
METADATA_EXPORT_SCRIPT_NAME,
|
||||
METADATA_IMPORT_SCRIPT_NAME,
|
||||
ScriptDataService,
|
||||
} from './core/data/processes/script-data.service';
|
||||
import { RemoteData } from './core/data/remote-data';
|
||||
import { BrowseDefinition } from './core/shared/browse-definition.model';
|
||||
import { ConfigurationProperty } from './core/shared/configuration-property.model';
|
||||
import { getFirstCompletedRemoteData } from './core/shared/operators';
|
||||
import { ThemedCreateCollectionParentSelectorComponent } from './shared/dso-selector/modal-wrappers/create-collection-parent-selector/themed-create-collection-parent-selector.component';
|
||||
import { ThemedCreateCommunityParentSelectorComponent } from './shared/dso-selector/modal-wrappers/create-community-parent-selector/themed-create-community-parent-selector.component';
|
||||
import { ThemedCreateItemParentSelectorComponent } from './shared/dso-selector/modal-wrappers/create-item-parent-selector/themed-create-item-parent-selector.component';
|
||||
import { ThemedEditCollectionSelectorComponent } from './shared/dso-selector/modal-wrappers/edit-collection-selector/themed-edit-collection-selector.component';
|
||||
import { ThemedEditCommunitySelectorComponent } from './shared/dso-selector/modal-wrappers/edit-community-selector/themed-edit-community-selector.component';
|
||||
import { ThemedEditItemSelectorComponent } from './shared/dso-selector/modal-wrappers/edit-item-selector/themed-edit-item-selector.component';
|
||||
import { ExportBatchSelectorComponent } from './shared/dso-selector/modal-wrappers/export-batch-selector/export-batch-selector.component';
|
||||
import { ExportMetadataSelectorComponent } from './shared/dso-selector/modal-wrappers/export-metadata-selector/export-metadata-selector.component';
|
||||
import { hasValue } from './shared/empty.util';
|
||||
import { MenuService } from './shared/menu/menu.service';
|
||||
import { MenuID } from './shared/menu/menu-id.model';
|
||||
import { LinkMenuItemModel } from './shared/menu/menu-item/models/link.model';
|
||||
import { OnClickMenuItemModel } from './shared/menu/menu-item/models/onclick.model';
|
||||
import { TextMenuItemModel } from './shared/menu/menu-item/models/text.model';
|
||||
import { MenuItemType } from './shared/menu/menu-item-type.model';
|
||||
import { MenuState } from './shared/menu/menu-state.model';
|
||||
|
||||
/**
|
||||
* Creates all of the app's menus
|
||||
*/
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class MenuResolverService {
|
||||
constructor(
|
||||
protected menuService: MenuService,
|
||||
protected browseService: BrowseService,
|
||||
protected authorizationService: AuthorizationDataService,
|
||||
protected modalService: NgbModal,
|
||||
protected scriptDataService: ScriptDataService,
|
||||
protected configurationDataService: ConfigurationDataService,
|
||||
protected authService: AuthService,
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize all menus
|
||||
*/
|
||||
resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean> {
|
||||
return combineLatest([
|
||||
this.createPublicMenu$(),
|
||||
this.createAdminMenuIfLoggedIn$(),
|
||||
]).pipe(
|
||||
map((menusDone: boolean[]) => menusDone.every(Boolean)),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Wait for a specific menu to appear
|
||||
* @param id the ID of the menu to wait for
|
||||
* @return an Observable that emits true as soon as the menu is created
|
||||
*/
|
||||
protected waitForMenu$(id: MenuID): Observable<boolean> {
|
||||
return this.menuService.getMenu(id).pipe(
|
||||
find((menu: MenuState) => hasValue(menu)),
|
||||
map(() => true),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize all menu sections and items for {@link MenuID.PUBLIC}
|
||||
*/
|
||||
createPublicMenu$(): Observable<boolean> {
|
||||
const menuList: any[] = [
|
||||
/* Communities & Collections tree */
|
||||
{
|
||||
id: `browse_global_communities_and_collections`,
|
||||
active: false,
|
||||
visible: true,
|
||||
index: 0,
|
||||
model: {
|
||||
type: MenuItemType.LINK,
|
||||
text: `menu.section.browse_global_communities_and_collections`,
|
||||
link: `/community-list`,
|
||||
} as LinkMenuItemModel,
|
||||
},
|
||||
|
||||
];
|
||||
// Read the different Browse-By types from config and add them to the browse menu
|
||||
this.browseService.getBrowseDefinitions()
|
||||
.pipe(getFirstCompletedRemoteData<PaginatedList<BrowseDefinition>>())
|
||||
.subscribe((browseDefListRD: RemoteData<PaginatedList<BrowseDefinition>>) => {
|
||||
if (browseDefListRD.hasSucceeded) {
|
||||
browseDefListRD.payload.page.forEach((browseDef: BrowseDefinition) => {
|
||||
menuList.push({
|
||||
id: `browse_global_by_${browseDef.id}`,
|
||||
parentID: 'browse_global',
|
||||
active: false,
|
||||
visible: true,
|
||||
model: {
|
||||
type: MenuItemType.LINK,
|
||||
text: `menu.section.browse_global_by_${browseDef.id}`,
|
||||
link: `/browse/${browseDef.id}`,
|
||||
} as LinkMenuItemModel,
|
||||
});
|
||||
});
|
||||
menuList.push(
|
||||
/* Browse */
|
||||
{
|
||||
id: 'browse_global',
|
||||
active: false,
|
||||
visible: true,
|
||||
index: 1,
|
||||
model: {
|
||||
type: MenuItemType.TEXT,
|
||||
text: 'menu.section.browse_global',
|
||||
} as TextMenuItemModel,
|
||||
},
|
||||
);
|
||||
}
|
||||
/* Add "Browse by Geolocation" map if enabled in configuration, with index = length to put it at the end of the list */
|
||||
if (environment.geospatialMapViewer.enableBrowseMap) {
|
||||
menuList.push(
|
||||
{
|
||||
id: `browse_global_geospatial_map`,
|
||||
parentID: 'browse_global',
|
||||
active: false,
|
||||
visible: true,
|
||||
index: menuList.length,
|
||||
model: {
|
||||
type: MenuItemType.LINK,
|
||||
text: `menu.section.browse_global_geospatial_map`,
|
||||
link: `/browse/map`,
|
||||
disabled: false,
|
||||
} as LinkMenuItemModel,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
menuList.forEach((menuSection) => this.menuService.addSection(MenuID.PUBLIC, Object.assign(menuSection, {
|
||||
shouldPersistOnRouteChange: true,
|
||||
})));
|
||||
});
|
||||
|
||||
return this.waitForMenu$(MenuID.PUBLIC);
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize all menu sections and items for {@link MenuID.ADMIN}, only if the user is logged in.
|
||||
*/
|
||||
createAdminMenuIfLoggedIn$() {
|
||||
return this.authService.isAuthenticated().pipe(
|
||||
mergeMap((isAuthenticated) => isAuthenticated ? this.createAdminMenu$() : observableOf(true)),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize all menu sections and items for {@link MenuID.ADMIN}
|
||||
*/
|
||||
createAdminMenu$() {
|
||||
this.createMainMenuSections();
|
||||
this.createSiteAdministratorMenuSections();
|
||||
this.createExportMenuSections();
|
||||
this.createImportMenuSections();
|
||||
this.createAccessControlMenuSections();
|
||||
this.createReportMenuSections();
|
||||
return this.waitForMenu$(MenuID.ADMIN);
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the main menu sections.
|
||||
* edit_community / edit_collection is only included if the current user is a Community or Collection admin
|
||||
*/
|
||||
createMainMenuSections() {
|
||||
combineLatest([
|
||||
this.authorizationService.isAuthorized(FeatureID.IsCollectionAdmin),
|
||||
this.authorizationService.isAuthorized(FeatureID.IsCommunityAdmin),
|
||||
this.authorizationService.isAuthorized(FeatureID.AdministratorOf),
|
||||
this.authorizationService.isAuthorized(FeatureID.CanSubmit),
|
||||
this.authorizationService.isAuthorized(FeatureID.CanEditItem),
|
||||
this.authorizationService.isAuthorized(FeatureID.CanSeeQA),
|
||||
this.authorizationService.isAuthorized(FeatureID.CoarNotifyEnabled),
|
||||
]).subscribe(([isCollectionAdmin, isCommunityAdmin, isSiteAdmin, canSubmit, canEditItem, canSeeQa, isCoarNotifyEnabled]) => {
|
||||
const newSubMenuList = [
|
||||
{
|
||||
id: 'new_community',
|
||||
parentID: 'new',
|
||||
active: false,
|
||||
visible: isCommunityAdmin,
|
||||
model: {
|
||||
type: MenuItemType.ONCLICK,
|
||||
text: 'menu.section.new_community',
|
||||
function: () => {
|
||||
this.modalService.open(ThemedCreateCommunityParentSelectorComponent);
|
||||
},
|
||||
} as OnClickMenuItemModel,
|
||||
},
|
||||
{
|
||||
id: 'new_collection',
|
||||
parentID: 'new',
|
||||
active: false,
|
||||
visible: isCommunityAdmin,
|
||||
model: {
|
||||
type: MenuItemType.ONCLICK,
|
||||
text: 'menu.section.new_collection',
|
||||
function: () => {
|
||||
this.modalService.open(ThemedCreateCollectionParentSelectorComponent);
|
||||
},
|
||||
} as OnClickMenuItemModel,
|
||||
},
|
||||
{
|
||||
id: 'new_item',
|
||||
parentID: 'new',
|
||||
active: false,
|
||||
visible: canSubmit,
|
||||
model: {
|
||||
type: MenuItemType.ONCLICK,
|
||||
text: 'menu.section.new_item',
|
||||
function: () => {
|
||||
this.modalService.open(ThemedCreateItemParentSelectorComponent);
|
||||
},
|
||||
} as OnClickMenuItemModel,
|
||||
},
|
||||
{
|
||||
id: 'new_process',
|
||||
parentID: 'new',
|
||||
active: false,
|
||||
visible: isSiteAdmin,
|
||||
model: {
|
||||
type: MenuItemType.LINK,
|
||||
text: 'menu.section.new_process',
|
||||
link: '/processes/new',
|
||||
} as LinkMenuItemModel,
|
||||
},/* ldn_services */
|
||||
{
|
||||
id: 'ldn_services_new',
|
||||
parentID: 'new',
|
||||
active: false,
|
||||
visible: isSiteAdmin && isCoarNotifyEnabled,
|
||||
model: {
|
||||
type: MenuItemType.LINK,
|
||||
text: 'menu.section.services_new',
|
||||
link: '/admin/ldn/services/new',
|
||||
} as LinkMenuItemModel,
|
||||
icon: '',
|
||||
},
|
||||
];
|
||||
const editSubMenuList = [
|
||||
/* Edit */
|
||||
{
|
||||
id: 'edit_community',
|
||||
parentID: 'edit',
|
||||
active: false,
|
||||
visible: isCommunityAdmin,
|
||||
model: {
|
||||
type: MenuItemType.ONCLICK,
|
||||
text: 'menu.section.edit_community',
|
||||
function: () => {
|
||||
this.modalService.open(ThemedEditCommunitySelectorComponent);
|
||||
},
|
||||
} as OnClickMenuItemModel,
|
||||
},
|
||||
{
|
||||
id: 'edit_collection',
|
||||
parentID: 'edit',
|
||||
active: false,
|
||||
visible: isCollectionAdmin,
|
||||
model: {
|
||||
type: MenuItemType.ONCLICK,
|
||||
text: 'menu.section.edit_collection',
|
||||
function: () => {
|
||||
this.modalService.open(ThemedEditCollectionSelectorComponent);
|
||||
},
|
||||
} as OnClickMenuItemModel,
|
||||
},
|
||||
{
|
||||
id: 'edit_item',
|
||||
parentID: 'edit',
|
||||
active: false,
|
||||
visible: canEditItem,
|
||||
model: {
|
||||
type: MenuItemType.ONCLICK,
|
||||
text: 'menu.section.edit_item',
|
||||
function: () => {
|
||||
this.modalService.open(ThemedEditItemSelectorComponent);
|
||||
},
|
||||
} as OnClickMenuItemModel,
|
||||
},
|
||||
];
|
||||
const newSubMenu = {
|
||||
id: 'new',
|
||||
active: false,
|
||||
visible: newSubMenuList.some(subMenu => subMenu.visible),
|
||||
model: {
|
||||
type: MenuItemType.TEXT,
|
||||
text: 'menu.section.new',
|
||||
} as TextMenuItemModel,
|
||||
icon: 'plus',
|
||||
index: 0,
|
||||
};
|
||||
const editSubMenu = {
|
||||
id: 'edit',
|
||||
active: false,
|
||||
visible: editSubMenuList.some(subMenu => subMenu.visible),
|
||||
model: {
|
||||
type: MenuItemType.TEXT,
|
||||
text: 'menu.section.edit',
|
||||
} as TextMenuItemModel,
|
||||
icon: 'pencil-alt',
|
||||
index: 1,
|
||||
};
|
||||
|
||||
const menuList = [
|
||||
...newSubMenuList,
|
||||
newSubMenu,
|
||||
...editSubMenuList,
|
||||
editSubMenu,
|
||||
// TODO: enable this menu item once the feature has been implemented
|
||||
// {
|
||||
// id: 'new_item_version',
|
||||
// parentID: 'new',
|
||||
// active: false,
|
||||
// visible: true,
|
||||
// model: {
|
||||
// type: MenuItemType.LINK,
|
||||
// text: 'menu.section.new_item_version',
|
||||
// link: ''
|
||||
// } as LinkMenuItemModel,
|
||||
// },
|
||||
|
||||
/* Statistics */
|
||||
// TODO: enable this menu item once the feature has been implemented
|
||||
// {
|
||||
// id: 'statistics_task',
|
||||
// active: false,
|
||||
// visible: true,
|
||||
// model: {
|
||||
// type: MenuItemType.LINK,
|
||||
// text: 'menu.section.statistics_task',
|
||||
// link: ''
|
||||
// } as LinkMenuItemModel,
|
||||
// icon: 'chart-bar',
|
||||
// index: 8
|
||||
// },
|
||||
|
||||
/* Control Panel */
|
||||
// TODO: enable this menu item once the feature has been implemented
|
||||
// {
|
||||
// id: 'control_panel',
|
||||
// active: false,
|
||||
// visible: isSiteAdmin,
|
||||
// model: {
|
||||
// type: MenuItemType.LINK,
|
||||
// text: 'menu.section.control_panel',
|
||||
// link: ''
|
||||
// } as LinkMenuItemModel,
|
||||
// icon: 'cogs',
|
||||
// index: 9
|
||||
// },
|
||||
|
||||
/* Processes */
|
||||
{
|
||||
id: 'processes',
|
||||
active: false,
|
||||
visible: isSiteAdmin,
|
||||
model: {
|
||||
type: MenuItemType.LINK,
|
||||
text: 'menu.section.processes',
|
||||
link: '/processes',
|
||||
} as LinkMenuItemModel,
|
||||
icon: 'terminal',
|
||||
index: 10,
|
||||
},
|
||||
/* COAR Notify section */
|
||||
{
|
||||
id: 'coar_notify',
|
||||
active: false,
|
||||
visible: isSiteAdmin && isCoarNotifyEnabled,
|
||||
model: {
|
||||
type: MenuItemType.TEXT,
|
||||
text: 'menu.section.coar_notify',
|
||||
} as TextMenuItemModel,
|
||||
icon: 'inbox',
|
||||
index: 13,
|
||||
},
|
||||
{
|
||||
id: 'notify_dashboard',
|
||||
active: false,
|
||||
parentID: 'coar_notify',
|
||||
visible: isSiteAdmin && isCoarNotifyEnabled,
|
||||
model: {
|
||||
type: MenuItemType.LINK,
|
||||
text: 'menu.section.notify_dashboard',
|
||||
link: '/admin/notify-dashboard',
|
||||
} as LinkMenuItemModel,
|
||||
},
|
||||
/* LDN Services */
|
||||
{
|
||||
id: 'ldn_services',
|
||||
active: false,
|
||||
parentID: 'coar_notify',
|
||||
visible: isSiteAdmin && isCoarNotifyEnabled,
|
||||
model: {
|
||||
type: MenuItemType.LINK,
|
||||
text: 'menu.section.services',
|
||||
link: '/admin/ldn/services',
|
||||
} as LinkMenuItemModel,
|
||||
},
|
||||
{
|
||||
id: 'health',
|
||||
active: false,
|
||||
visible: isSiteAdmin,
|
||||
model: {
|
||||
type: MenuItemType.LINK,
|
||||
text: 'menu.section.health',
|
||||
link: '/health',
|
||||
} as LinkMenuItemModel,
|
||||
icon: 'heartbeat',
|
||||
index: 11,
|
||||
},
|
||||
/* Notifications */
|
||||
{
|
||||
id: 'notifications',
|
||||
active: false,
|
||||
visible: canSeeQa || isSiteAdmin,
|
||||
model: {
|
||||
type: MenuItemType.TEXT,
|
||||
text: 'menu.section.notifications',
|
||||
} as TextMenuItemModel,
|
||||
icon: 'bell',
|
||||
index: 4,
|
||||
},
|
||||
{
|
||||
id: 'notifications_quality-assurance',
|
||||
parentID: 'notifications',
|
||||
active: false,
|
||||
visible: canSeeQa,
|
||||
model: {
|
||||
type: MenuItemType.LINK,
|
||||
text: 'menu.section.quality-assurance',
|
||||
link: '/notifications/quality-assurance',
|
||||
} as LinkMenuItemModel,
|
||||
},
|
||||
{
|
||||
id: 'notifications_publication-claim',
|
||||
parentID: 'notifications',
|
||||
active: false,
|
||||
visible: isSiteAdmin,
|
||||
model: {
|
||||
type: MenuItemType.LINK,
|
||||
text: 'menu.section.notifications_publication-claim',
|
||||
link: '/admin/notifications/' + PUBLICATION_CLAIMS_PATH,
|
||||
} as LinkMenuItemModel,
|
||||
},
|
||||
/* Admin Search */
|
||||
];
|
||||
menuList.forEach((menuSection) => this.menuService.addSection(MenuID.ADMIN, Object.assign(menuSection, {
|
||||
shouldPersistOnRouteChange: true,
|
||||
})));
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Create menu sections dependent on whether or not the current user is a site administrator and on whether or not
|
||||
* the export scripts exist and the current user is allowed to execute them
|
||||
*/
|
||||
createExportMenuSections() {
|
||||
const menuList = [
|
||||
// TODO: enable this menu item once the feature has been implemented
|
||||
// {
|
||||
// id: 'export_community',
|
||||
// parentID: 'export',
|
||||
// active: false,
|
||||
// visible: true,
|
||||
// model: {
|
||||
// type: MenuItemType.LINK,
|
||||
// text: 'menu.section.export_community',
|
||||
// link: ''
|
||||
// } as LinkMenuItemModel,
|
||||
// shouldPersistOnRouteChange: true
|
||||
// },
|
||||
// TODO: enable this menu item once the feature has been implemented
|
||||
// {
|
||||
// id: 'export_collection',
|
||||
// parentID: 'export',
|
||||
// active: false,
|
||||
// visible: true,
|
||||
// model: {
|
||||
// type: MenuItemType.LINK,
|
||||
// text: 'menu.section.export_collection',
|
||||
// link: ''
|
||||
// } as LinkMenuItemModel,
|
||||
// shouldPersistOnRouteChange: true
|
||||
// },
|
||||
// TODO: enable this menu item once the feature has been implemented
|
||||
// {
|
||||
// id: 'export_item',
|
||||
// parentID: 'export',
|
||||
// active: false,
|
||||
// visible: true,
|
||||
// model: {
|
||||
// type: MenuItemType.LINK,
|
||||
// text: 'menu.section.export_item',
|
||||
// link: ''
|
||||
// } as LinkMenuItemModel,
|
||||
// shouldPersistOnRouteChange: true
|
||||
// },
|
||||
];
|
||||
menuList.forEach((menuSection) => this.menuService.addSection(MenuID.ADMIN, menuSection));
|
||||
|
||||
observableCombineLatest([
|
||||
this.authorizationService.isAuthorized(FeatureID.AdministratorOf),
|
||||
this.scriptDataService.scriptWithNameExistsAndCanExecute(METADATA_EXPORT_SCRIPT_NAME),
|
||||
]).pipe(
|
||||
filter(([authorized, metadataExportScriptExists]: boolean[]) => authorized && metadataExportScriptExists),
|
||||
take(1),
|
||||
).subscribe(() => {
|
||||
// Hides the export menu for unauthorised people
|
||||
// If in the future more sub-menus are added,
|
||||
// it should be reviewed if they need to be in this subscribe
|
||||
this.menuService.addSection(MenuID.ADMIN, {
|
||||
id: 'export',
|
||||
active: false,
|
||||
visible: true,
|
||||
model: {
|
||||
type: MenuItemType.TEXT,
|
||||
text: 'menu.section.export',
|
||||
} as TextMenuItemModel,
|
||||
icon: 'file-export',
|
||||
index: 3,
|
||||
shouldPersistOnRouteChange: true,
|
||||
});
|
||||
this.menuService.addSection(MenuID.ADMIN, {
|
||||
id: 'export_metadata',
|
||||
parentID: 'export',
|
||||
active: true,
|
||||
visible: true,
|
||||
model: {
|
||||
type: MenuItemType.ONCLICK,
|
||||
text: 'menu.section.export_metadata',
|
||||
function: () => {
|
||||
this.modalService.open(ExportMetadataSelectorComponent);
|
||||
},
|
||||
} as OnClickMenuItemModel,
|
||||
shouldPersistOnRouteChange: true,
|
||||
});
|
||||
this.menuService.addSection(MenuID.ADMIN, {
|
||||
id: 'export_batch',
|
||||
parentID: 'export',
|
||||
active: false,
|
||||
visible: true,
|
||||
model: {
|
||||
type: MenuItemType.ONCLICK,
|
||||
text: 'menu.section.export_batch',
|
||||
function: () => {
|
||||
this.modalService.open(ExportBatchSelectorComponent);
|
||||
},
|
||||
} as OnClickMenuItemModel,
|
||||
shouldPersistOnRouteChange: true,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Create menu sections dependent on whether or not the current user is a site administrator and on whether or not
|
||||
* the import scripts exist and the current user is allowed to execute them
|
||||
*/
|
||||
createImportMenuSections() {
|
||||
const menuList = [];
|
||||
menuList.forEach((menuSection) => this.menuService.addSection(MenuID.ADMIN, menuSection));
|
||||
|
||||
observableCombineLatest([
|
||||
this.authorizationService.isAuthorized(FeatureID.AdministratorOf),
|
||||
this.scriptDataService.scriptWithNameExistsAndCanExecute(METADATA_IMPORT_SCRIPT_NAME),
|
||||
]).pipe(
|
||||
filter(([authorized, metadataImportScriptExists]: boolean[]) => authorized && metadataImportScriptExists),
|
||||
take(1),
|
||||
).subscribe(() => {
|
||||
// Hides the import menu for unauthorised people
|
||||
// If in the future more sub-menus are added,
|
||||
// it should be reviewed if they need to be in this subscribe
|
||||
this.menuService.addSection(MenuID.ADMIN, {
|
||||
id: 'import',
|
||||
active: false,
|
||||
visible: true,
|
||||
model: {
|
||||
type: MenuItemType.TEXT,
|
||||
text: 'menu.section.import',
|
||||
} as TextMenuItemModel,
|
||||
icon: 'file-import',
|
||||
index: 2,
|
||||
shouldPersistOnRouteChange: true,
|
||||
});
|
||||
this.menuService.addSection(MenuID.ADMIN, {
|
||||
id: 'import_metadata',
|
||||
parentID: 'import',
|
||||
active: true,
|
||||
visible: true,
|
||||
model: {
|
||||
type: MenuItemType.LINK,
|
||||
text: 'menu.section.import_metadata',
|
||||
link: '/admin/metadata-import',
|
||||
} as LinkMenuItemModel,
|
||||
shouldPersistOnRouteChange: true,
|
||||
});
|
||||
this.menuService.addSection(MenuID.ADMIN, {
|
||||
id: 'import_batch',
|
||||
parentID: 'import',
|
||||
active: false,
|
||||
visible: true,
|
||||
model: {
|
||||
type: MenuItemType.LINK,
|
||||
text: 'menu.section.import_batch',
|
||||
link: '/admin/batch-import',
|
||||
} as LinkMenuItemModel,
|
||||
shouldPersistOnRouteChange: true,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Create menu sections dependent on whether or not the current user is a site administrator
|
||||
*/
|
||||
createSiteAdministratorMenuSections() {
|
||||
this.authorizationService.isAuthorized(FeatureID.AdministratorOf)
|
||||
.subscribe((authorized) => {
|
||||
const menuList = [
|
||||
{
|
||||
id: 'admin_search',
|
||||
active: false,
|
||||
visible: authorized,
|
||||
model: {
|
||||
type: MenuItemType.LINK,
|
||||
text: 'menu.section.admin_search',
|
||||
link: '/admin/search',
|
||||
} as LinkMenuItemModel,
|
||||
icon: 'search',
|
||||
index: 5,
|
||||
},
|
||||
/* Registries */
|
||||
{
|
||||
id: 'registries',
|
||||
active: false,
|
||||
visible: authorized,
|
||||
model: {
|
||||
type: MenuItemType.TEXT,
|
||||
text: 'menu.section.registries',
|
||||
} as TextMenuItemModel,
|
||||
icon: 'list',
|
||||
index: 6,
|
||||
},
|
||||
{
|
||||
id: 'registries_metadata',
|
||||
parentID: 'registries',
|
||||
active: false,
|
||||
visible: authorized,
|
||||
model: {
|
||||
type: MenuItemType.LINK,
|
||||
text: 'menu.section.registries_metadata',
|
||||
link: 'admin/registries/metadata',
|
||||
} as LinkMenuItemModel,
|
||||
},
|
||||
{
|
||||
id: 'registries_format',
|
||||
parentID: 'registries',
|
||||
active: false,
|
||||
visible: authorized,
|
||||
model: {
|
||||
type: MenuItemType.LINK,
|
||||
text: 'menu.section.registries_format',
|
||||
link: 'admin/registries/bitstream-formats',
|
||||
} as LinkMenuItemModel,
|
||||
},
|
||||
|
||||
/* Curation tasks */
|
||||
{
|
||||
id: 'curation_tasks',
|
||||
active: false,
|
||||
visible: authorized,
|
||||
model: {
|
||||
type: MenuItemType.LINK,
|
||||
text: 'menu.section.curation_task',
|
||||
link: 'admin/curation-tasks',
|
||||
} as LinkMenuItemModel,
|
||||
icon: 'filter',
|
||||
index: 7,
|
||||
},
|
||||
|
||||
/* Workflow */
|
||||
{
|
||||
id: 'workflow',
|
||||
active: false,
|
||||
visible: authorized,
|
||||
model: {
|
||||
type: MenuItemType.LINK,
|
||||
text: 'menu.section.workflow',
|
||||
link: '/admin/workflow',
|
||||
} as LinkMenuItemModel,
|
||||
icon: 'user-check',
|
||||
index: 11,
|
||||
},
|
||||
{
|
||||
id: 'system_wide_alert',
|
||||
active: false,
|
||||
visible: authorized,
|
||||
model: {
|
||||
type: MenuItemType.LINK,
|
||||
text: 'menu.section.system-wide-alert',
|
||||
link: '/admin/system-wide-alert',
|
||||
} as LinkMenuItemModel,
|
||||
icon: 'exclamation-circle',
|
||||
index: 12,
|
||||
},
|
||||
];
|
||||
|
||||
menuList.forEach((menuSection) => this.menuService.addSection(MenuID.ADMIN, Object.assign(menuSection, {
|
||||
shouldPersistOnRouteChange: true,
|
||||
})));
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Create menu sections dependent on whether or not the current user can manage access control groups
|
||||
*/
|
||||
createAccessControlMenuSections() {
|
||||
observableCombineLatest([
|
||||
this.authorizationService.isAuthorized(FeatureID.AdministratorOf),
|
||||
this.authorizationService.isAuthorized(FeatureID.CanManageGroups),
|
||||
]).subscribe(([isSiteAdmin, canManageGroups]) => {
|
||||
const menuList = [
|
||||
/* Access Control */
|
||||
{
|
||||
id: 'access_control_people',
|
||||
parentID: 'access_control',
|
||||
active: false,
|
||||
visible: isSiteAdmin,
|
||||
model: {
|
||||
type: MenuItemType.LINK,
|
||||
text: 'menu.section.access_control_people',
|
||||
link: '/access-control/epeople',
|
||||
} as LinkMenuItemModel,
|
||||
},
|
||||
{
|
||||
id: 'access_control_groups',
|
||||
parentID: 'access_control',
|
||||
active: false,
|
||||
visible: canManageGroups,
|
||||
model: {
|
||||
type: MenuItemType.LINK,
|
||||
text: 'menu.section.access_control_groups',
|
||||
link: '/access-control/groups',
|
||||
} as LinkMenuItemModel,
|
||||
},
|
||||
{
|
||||
id: 'access_control_bulk',
|
||||
parentID: 'access_control',
|
||||
active: false,
|
||||
visible: isSiteAdmin,
|
||||
model: {
|
||||
type: MenuItemType.LINK,
|
||||
text: 'menu.section.access_control_bulk',
|
||||
link: '/access-control/bulk-access',
|
||||
} as LinkMenuItemModel,
|
||||
},
|
||||
// TODO: enable this menu item once the feature has been implemented
|
||||
// {
|
||||
// id: 'access_control_authorizations',
|
||||
// parentID: 'access_control',
|
||||
// active: false,
|
||||
// visible: authorized,
|
||||
// model: {
|
||||
// type: MenuItemType.LINK,
|
||||
// text: 'menu.section.access_control_authorizations',
|
||||
// link: ''
|
||||
// } as LinkMenuItemModel,
|
||||
// },
|
||||
{
|
||||
id: 'access_control',
|
||||
active: false,
|
||||
visible: canManageGroups || isSiteAdmin,
|
||||
model: {
|
||||
type: MenuItemType.TEXT,
|
||||
text: 'menu.section.access_control',
|
||||
} as TextMenuItemModel,
|
||||
icon: 'key',
|
||||
index: 4,
|
||||
},
|
||||
];
|
||||
|
||||
menuList.forEach((menuSection) => this.menuService.addSection(MenuID.ADMIN, Object.assign(menuSection, {
|
||||
shouldPersistOnRouteChange: true,
|
||||
})));
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Create menu sections dependent on whether or not the current user is a site administrator
|
||||
*/
|
||||
createReportMenuSections() {
|
||||
observableCombineLatest([
|
||||
this.configurationDataService.findByPropertyName('contentreport.enable').pipe(
|
||||
getFirstCompletedRemoteData(),
|
||||
map((res: RemoteData<ConfigurationProperty>) => res.hasSucceeded && res.payload && res.payload.values[0] === 'true'),
|
||||
),
|
||||
this.authorizationService.isAuthorized(FeatureID.AdministratorOf),
|
||||
]).subscribe(([isSiteAdmin]) => {
|
||||
const menuList = [
|
||||
{
|
||||
id: 'reports',
|
||||
active: false,
|
||||
visible: isSiteAdmin,
|
||||
model: {
|
||||
type: MenuItemType.TEXT,
|
||||
text: 'menu.section.reports',
|
||||
} as TextMenuItemModel,
|
||||
icon: 'file-alt',
|
||||
index: 5,
|
||||
},
|
||||
/* Collections Report */
|
||||
{
|
||||
id: 'reports_collections',
|
||||
parentID: 'reports',
|
||||
active: false,
|
||||
visible: isSiteAdmin,
|
||||
model: {
|
||||
type: MenuItemType.LINK,
|
||||
text: 'menu.section.reports.collections',
|
||||
link: '/admin/reports/collections',
|
||||
} as LinkMenuItemModel,
|
||||
icon: 'user-check',
|
||||
},
|
||||
/* Queries Report */
|
||||
{
|
||||
id: 'reports_queries',
|
||||
parentID: 'reports',
|
||||
active: false,
|
||||
visible: isSiteAdmin,
|
||||
model: {
|
||||
type: MenuItemType.LINK,
|
||||
text: 'menu.section.reports.queries',
|
||||
link: '/admin/reports/queries',
|
||||
} as LinkMenuItemModel,
|
||||
icon: 'user-check',
|
||||
},
|
||||
];
|
||||
|
||||
menuList.forEach((menuSection) => this.menuService.addSection(MenuID.ADMIN, Object.assign(menuSection, {
|
||||
shouldPersistOnRouteChange: true,
|
||||
})));
|
||||
});
|
||||
}
|
||||
}
|
@@ -1,440 +0,0 @@
|
||||
import { NO_ERRORS_SCHEMA } from '@angular/core';
|
||||
import {
|
||||
TestBed,
|
||||
waitForAsync,
|
||||
} from '@angular/core/testing';
|
||||
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
|
||||
import { RouterTestingModule } from '@angular/router/testing';
|
||||
import {
|
||||
NgbModal,
|
||||
NgbModalRef,
|
||||
} from '@ng-bootstrap/ng-bootstrap';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { cold } from 'jasmine-marbles';
|
||||
import { of as observableOf } from 'rxjs';
|
||||
|
||||
import { AdminSidebarComponent } from './admin/admin-sidebar/admin-sidebar.component';
|
||||
import { BrowseService } from './core/browse/browse.service';
|
||||
import { ConfigurationDataService } from './core/data/configuration-data.service';
|
||||
import { AuthorizationDataService } from './core/data/feature-authorization/authorization-data.service';
|
||||
import { FeatureID } from './core/data/feature-authorization/feature-id';
|
||||
import { ScriptDataService } from './core/data/processes/script-data.service';
|
||||
import { MenuService } from './shared/menu/menu.service';
|
||||
import { MenuID } from './shared/menu/menu-id.model';
|
||||
import { createSuccessfulRemoteDataObject$ } from './shared/remote-data.utils';
|
||||
import { ConfigurationDataServiceStub } from './shared/testing/configuration-data.service.stub';
|
||||
import { MenuServiceStub } from './shared/testing/menu-service.stub';
|
||||
import { createPaginatedList } from './shared/testing/utils.test';
|
||||
import createSpy = jasmine.createSpy;
|
||||
import { AuthService } from './core/auth/auth.service';
|
||||
import { MenuResolverService } from './menu-resolver.service';
|
||||
import { AuthServiceStub } from './shared/testing/auth-service.stub';
|
||||
|
||||
const BOOLEAN = { t: true, f: false };
|
||||
const MENU_STATE = {
|
||||
id: 'some menu',
|
||||
};
|
||||
const BROWSE_DEFINITIONS = [
|
||||
{ id: 'definition1' },
|
||||
{ id: 'definition2' },
|
||||
{ id: 'definition3' },
|
||||
];
|
||||
|
||||
describe('menuResolver', () => {
|
||||
let resolver: MenuResolverService;
|
||||
|
||||
let menuService;
|
||||
let browseService;
|
||||
let authorizationService;
|
||||
let scriptService;
|
||||
let mockNgbModal;
|
||||
let configurationDataService;
|
||||
|
||||
beforeEach(waitForAsync(() => {
|
||||
menuService = new MenuServiceStub();
|
||||
spyOn(menuService, 'getMenu').and.returnValue(observableOf(MENU_STATE));
|
||||
spyOn(menuService, 'addSection');
|
||||
|
||||
browseService = jasmine.createSpyObj('browseService', {
|
||||
getBrowseDefinitions: createSuccessfulRemoteDataObject$(createPaginatedList(BROWSE_DEFINITIONS)),
|
||||
});
|
||||
authorizationService = jasmine.createSpyObj('authorizationService', {
|
||||
isAuthorized: observableOf(true),
|
||||
});
|
||||
scriptService = jasmine.createSpyObj('scriptService', {
|
||||
scriptWithNameExistsAndCanExecute: observableOf(true),
|
||||
});
|
||||
mockNgbModal = {
|
||||
open: jasmine.createSpy('open').and.returnValue(
|
||||
{ componentInstance: {}, closed: observableOf({}) } as NgbModalRef,
|
||||
),
|
||||
};
|
||||
|
||||
configurationDataService = new ConfigurationDataServiceStub();
|
||||
spyOn(configurationDataService, 'findByPropertyName').and.returnValue(observableOf(true));
|
||||
|
||||
TestBed.configureTestingModule({
|
||||
imports: [TranslateModule.forRoot(), NoopAnimationsModule, RouterTestingModule, AdminSidebarComponent],
|
||||
providers: [
|
||||
{ provide: MenuService, useValue: menuService },
|
||||
{ provide: BrowseService, useValue: browseService },
|
||||
{ provide: AuthorizationDataService, useValue: authorizationService },
|
||||
{ provide: ScriptDataService, useValue: scriptService },
|
||||
{ provide: ConfigurationDataService, useValue: configurationDataService },
|
||||
{ provide: NgbModal, useValue: mockNgbModal },
|
||||
{ provide: AuthService, useValue: AuthServiceStub },
|
||||
MenuResolverService,
|
||||
],
|
||||
schemas: [NO_ERRORS_SCHEMA],
|
||||
});
|
||||
resolver = TestBed.inject(MenuResolverService);
|
||||
}));
|
||||
|
||||
it('should be created', () => {
|
||||
expect(resolver).toBeTruthy();
|
||||
});
|
||||
|
||||
describe('resolve', () => {
|
||||
it('should create all menus', (done) => {
|
||||
spyOn(resolver, 'createPublicMenu$').and.returnValue(observableOf(true));
|
||||
spyOn(resolver, 'createAdminMenuIfLoggedIn$').and.returnValue(observableOf(true));
|
||||
|
||||
resolver.resolve(null, null).subscribe(resolved => {
|
||||
expect(resolved).toBeTrue();
|
||||
expect(resolver.createPublicMenu$).toHaveBeenCalled();
|
||||
expect(resolver.createAdminMenuIfLoggedIn$).toHaveBeenCalled();
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should return an Observable that emits true as soon as all menus are created', () => {
|
||||
spyOn(resolver, 'createPublicMenu$').and.returnValue(cold('--(t|)', BOOLEAN));
|
||||
spyOn(resolver, 'createAdminMenuIfLoggedIn$').and.returnValue(cold('----(t|)', BOOLEAN));
|
||||
|
||||
expect(resolver.resolve(null, null)).toBeObservable(cold('----(t|)', BOOLEAN));
|
||||
});
|
||||
});
|
||||
|
||||
describe('createPublicMenu$', () => {
|
||||
it('should retrieve the menu by ID return an Observable that emits true as soon as it is created', () => {
|
||||
(menuService as any).getMenu.and.returnValue(cold('--u--m--', {
|
||||
u: undefined,
|
||||
m: MENU_STATE,
|
||||
}));
|
||||
|
||||
expect(resolver.createPublicMenu$()).toBeObservable(cold('-----(t|)', BOOLEAN));
|
||||
expect(menuService.getMenu).toHaveBeenCalledOnceWith(MenuID.PUBLIC);
|
||||
});
|
||||
|
||||
describe('contents', () => {
|
||||
beforeEach((done) => {
|
||||
resolver.createPublicMenu$().subscribe((_) => {
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should include community list link', () => {
|
||||
expect(menuService.addSection).toHaveBeenCalledWith(MenuID.PUBLIC, jasmine.objectContaining({
|
||||
id: 'browse_global_communities_and_collections', visible: true,
|
||||
}));
|
||||
});
|
||||
|
||||
it('should include browse dropdown', () => {
|
||||
expect(menuService.addSection).toHaveBeenCalledWith(MenuID.PUBLIC, jasmine.objectContaining({
|
||||
id: 'browse_global_by_definition1', parentID: 'browse_global', visible: true,
|
||||
}));
|
||||
expect(menuService.addSection).toHaveBeenCalledWith(MenuID.PUBLIC, jasmine.objectContaining({
|
||||
id: 'browse_global_by_definition2', parentID: 'browse_global', visible: true,
|
||||
}));
|
||||
expect(menuService.addSection).toHaveBeenCalledWith(MenuID.PUBLIC, jasmine.objectContaining({
|
||||
id: 'browse_global_by_definition3', parentID: 'browse_global', visible: true,
|
||||
}));
|
||||
|
||||
expect(menuService.addSection).toHaveBeenCalledWith(MenuID.PUBLIC, jasmine.objectContaining({
|
||||
id: 'browse_global', visible: true,
|
||||
}));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('createAdminMenu$', () => {
|
||||
const dontShowAdminSections = () => {
|
||||
it('should not show site admin section', () => {
|
||||
expect(menuService.addSection).toHaveBeenCalledWith(MenuID.ADMIN, jasmine.objectContaining({
|
||||
id: 'admin_search', visible: false,
|
||||
}));
|
||||
expect(menuService.addSection).toHaveBeenCalledWith(MenuID.ADMIN, jasmine.objectContaining({
|
||||
id: 'registries', visible: false,
|
||||
}));
|
||||
expect(menuService.addSection).toHaveBeenCalledWith(MenuID.ADMIN, jasmine.objectContaining({
|
||||
parentID: 'registries', visible: false,
|
||||
}));
|
||||
expect(menuService.addSection).toHaveBeenCalledWith(MenuID.ADMIN, jasmine.objectContaining({
|
||||
id: 'curation_tasks', visible: false,
|
||||
}));
|
||||
expect(menuService.addSection).toHaveBeenCalledWith(MenuID.ADMIN, jasmine.objectContaining({
|
||||
id: 'workflow', visible: false,
|
||||
}));
|
||||
});
|
||||
|
||||
it('should not show access control section', () => {
|
||||
expect(menuService.addSection).toHaveBeenCalledWith(MenuID.ADMIN, jasmine.objectContaining({
|
||||
id: 'access_control', visible: false,
|
||||
}));
|
||||
expect(menuService.addSection).toHaveBeenCalledWith(MenuID.ADMIN, jasmine.objectContaining({
|
||||
parentID: 'access_control', visible: false,
|
||||
}));
|
||||
});
|
||||
|
||||
// We check that the menu section has not been called with visible set to true
|
||||
// The reason why we don't check if it has been called with visible set to false
|
||||
// Is because the function does not get called unless a user is authorised
|
||||
it('should not show the import section', () => {
|
||||
expect(menuService.addSection).not.toHaveBeenCalledWith(MenuID.ADMIN, jasmine.objectContaining({
|
||||
id: 'import', visible: true,
|
||||
}));
|
||||
});
|
||||
|
||||
// We check that the menu section has not been called with visible set to true
|
||||
// The reason why we don't check if it has been called with visible set to false
|
||||
// Is because the function does not get called unless a user is authorised
|
||||
it('should not show the export section', () => {
|
||||
expect(menuService.addSection).not.toHaveBeenCalledWith(MenuID.ADMIN, jasmine.objectContaining({
|
||||
id: 'export', visible: true,
|
||||
}));
|
||||
});
|
||||
};
|
||||
|
||||
const dontShowNewSection = () => {
|
||||
it('should not show the "New" section', () => {
|
||||
expect(menuService.addSection).toHaveBeenCalledWith(MenuID.ADMIN, jasmine.objectContaining({
|
||||
id: 'new_community', visible: false,
|
||||
}));
|
||||
expect(menuService.addSection).toHaveBeenCalledWith(MenuID.ADMIN, jasmine.objectContaining({
|
||||
id: 'new_collection', visible: false,
|
||||
}));
|
||||
expect(menuService.addSection).toHaveBeenCalledWith(MenuID.ADMIN, jasmine.objectContaining({
|
||||
id: 'new_item', visible: false,
|
||||
}));
|
||||
expect(menuService.addSection).toHaveBeenCalledWith(MenuID.ADMIN, jasmine.objectContaining({
|
||||
id: 'new', visible: false,
|
||||
}));
|
||||
});
|
||||
};
|
||||
|
||||
const dontShowEditSection = () => {
|
||||
it('should not show the "Edit" section', () => {
|
||||
expect(menuService.addSection).toHaveBeenCalledWith(MenuID.ADMIN, jasmine.objectContaining({
|
||||
id: 'edit_community', visible: false,
|
||||
}));
|
||||
expect(menuService.addSection).toHaveBeenCalledWith(MenuID.ADMIN, jasmine.objectContaining({
|
||||
id: 'edit_collection', visible: false,
|
||||
}));
|
||||
expect(menuService.addSection).toHaveBeenCalledWith(MenuID.ADMIN, jasmine.objectContaining({
|
||||
id: 'edit_item', visible: false,
|
||||
}));
|
||||
expect(menuService.addSection).toHaveBeenCalledWith(MenuID.ADMIN, jasmine.objectContaining({
|
||||
id: 'edit', visible: false,
|
||||
}));
|
||||
});
|
||||
};
|
||||
|
||||
it('should retrieve the menu by ID return an Observable that emits true as soon as it is created', () => {
|
||||
(menuService as any).getMenu.and.returnValue(cold('--u--m', {
|
||||
u: undefined,
|
||||
m: MENU_STATE,
|
||||
}));
|
||||
|
||||
expect(resolver.createAdminMenu$()).toBeObservable(cold('-----(t|)', BOOLEAN));
|
||||
expect(menuService.getMenu).toHaveBeenCalledOnceWith(MenuID.ADMIN);
|
||||
});
|
||||
|
||||
describe('for regular user', () => {
|
||||
beforeEach(() => {
|
||||
authorizationService.isAuthorized = createSpy('isAuthorized').and.callFake((featureID) => {
|
||||
return observableOf(false);
|
||||
});
|
||||
});
|
||||
|
||||
beforeEach((done) => {
|
||||
resolver.createAdminMenu$().subscribe((_) => {
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
dontShowAdminSections();
|
||||
dontShowNewSection();
|
||||
dontShowEditSection();
|
||||
});
|
||||
|
||||
describe('regular user who can submit', () => {
|
||||
beforeEach(() => {
|
||||
authorizationService.isAuthorized = createSpy('isAuthorized')
|
||||
.and.callFake((featureID: FeatureID) => {
|
||||
return observableOf(featureID === FeatureID.CanSubmit);
|
||||
});
|
||||
});
|
||||
|
||||
beforeEach((done) => {
|
||||
resolver.createAdminMenu$().subscribe((_) => {
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should show "New Item" section', () => {
|
||||
expect(menuService.addSection).toHaveBeenCalledWith(MenuID.ADMIN, jasmine.objectContaining({
|
||||
id: 'new_item', visible: true,
|
||||
}));
|
||||
expect(menuService.addSection).toHaveBeenCalledWith(MenuID.ADMIN, jasmine.objectContaining({
|
||||
id: 'new', visible: true,
|
||||
}));
|
||||
});
|
||||
|
||||
dontShowAdminSections();
|
||||
dontShowEditSection();
|
||||
});
|
||||
|
||||
describe('regular user who can edit items', () => {
|
||||
beforeEach(() => {
|
||||
authorizationService.isAuthorized = createSpy('isAuthorized')
|
||||
.and.callFake((featureID: FeatureID) => {
|
||||
return observableOf(featureID === FeatureID.CanEditItem);
|
||||
});
|
||||
});
|
||||
|
||||
beforeEach((done) => {
|
||||
resolver.createAdminMenu$().subscribe((_) => {
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should show "Edit Item" section', () => {
|
||||
expect(menuService.addSection).toHaveBeenCalledWith(MenuID.ADMIN, jasmine.objectContaining({
|
||||
id: 'edit_item', visible: true,
|
||||
}));
|
||||
expect(menuService.addSection).toHaveBeenCalledWith(MenuID.ADMIN, jasmine.objectContaining({
|
||||
id: 'edit', visible: true,
|
||||
}));
|
||||
});
|
||||
|
||||
dontShowAdminSections();
|
||||
dontShowNewSection();
|
||||
});
|
||||
|
||||
describe('for site admin', () => {
|
||||
beforeEach(() => {
|
||||
authorizationService.isAuthorized = createSpy('isAuthorized').and.callFake((featureID: FeatureID) => {
|
||||
return observableOf(featureID === FeatureID.AdministratorOf);
|
||||
});
|
||||
});
|
||||
|
||||
beforeEach((done) => {
|
||||
resolver.createAdminMenu$().subscribe((_) => {
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should show new_process', () => {
|
||||
expect(menuService.addSection).toHaveBeenCalledWith(MenuID.ADMIN, jasmine.objectContaining({
|
||||
id: 'new_process', visible: true,
|
||||
}));
|
||||
});
|
||||
|
||||
it('should contain site admin section', () => {
|
||||
expect(menuService.addSection).toHaveBeenCalledWith(MenuID.ADMIN, jasmine.objectContaining({
|
||||
id: 'admin_search', visible: true,
|
||||
}));
|
||||
expect(menuService.addSection).toHaveBeenCalledWith(MenuID.ADMIN, jasmine.objectContaining({
|
||||
id: 'registries', visible: true,
|
||||
}));
|
||||
expect(menuService.addSection).toHaveBeenCalledWith(MenuID.ADMIN, jasmine.objectContaining({
|
||||
parentID: 'registries', visible: true,
|
||||
}));
|
||||
expect(menuService.addSection).toHaveBeenCalledWith(MenuID.ADMIN, jasmine.objectContaining({
|
||||
id: 'curation_tasks', visible: true,
|
||||
}));
|
||||
expect(menuService.addSection).toHaveBeenCalledWith(MenuID.ADMIN, jasmine.objectContaining({
|
||||
id: 'workflow', visible: true,
|
||||
}));
|
||||
expect(menuService.addSection).toHaveBeenCalledWith(MenuID.ADMIN, jasmine.objectContaining({
|
||||
id: 'workflow', visible: true,
|
||||
}));
|
||||
expect(menuService.addSection).toHaveBeenCalledWith(MenuID.ADMIN, jasmine.objectContaining({
|
||||
id: 'import', visible: true,
|
||||
}));
|
||||
expect(menuService.addSection).toHaveBeenCalledWith(MenuID.ADMIN, jasmine.objectContaining({
|
||||
id: 'import_batch', parentID: 'import', visible: true,
|
||||
}));
|
||||
expect(menuService.addSection).toHaveBeenCalledWith(MenuID.ADMIN, jasmine.objectContaining({
|
||||
id: 'export', visible: true,
|
||||
}));
|
||||
expect(menuService.addSection).toHaveBeenCalledWith(MenuID.ADMIN, jasmine.objectContaining({
|
||||
id: 'export_batch', parentID: 'export', visible: true,
|
||||
}));
|
||||
});
|
||||
});
|
||||
|
||||
describe('for community admin', () => {
|
||||
beforeEach(() => {
|
||||
authorizationService.isAuthorized = createSpy('isAuthorized').and.callFake((featureID: FeatureID) => {
|
||||
return observableOf(featureID === FeatureID.IsCommunityAdmin);
|
||||
});
|
||||
});
|
||||
|
||||
beforeEach((done) => {
|
||||
resolver.createAdminMenu$().subscribe((_) => {
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should show edit_community', () => {
|
||||
expect(menuService.addSection).toHaveBeenCalledWith(MenuID.ADMIN, jasmine.objectContaining({
|
||||
id: 'edit_community', visible: true,
|
||||
}));
|
||||
});
|
||||
});
|
||||
|
||||
describe('for collection admin', () => {
|
||||
beforeEach(() => {
|
||||
authorizationService.isAuthorized = createSpy('isAuthorized').and.callFake((featureID: FeatureID) => {
|
||||
return observableOf(featureID === FeatureID.IsCollectionAdmin);
|
||||
});
|
||||
});
|
||||
|
||||
beforeEach((done) => {
|
||||
resolver.createAdminMenu$().subscribe((_) => {
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should show edit_collection', () => {
|
||||
expect(menuService.addSection).toHaveBeenCalledWith(MenuID.ADMIN, jasmine.objectContaining({
|
||||
id: 'edit_collection', visible: true,
|
||||
}));
|
||||
});
|
||||
});
|
||||
|
||||
describe('for group admin', () => {
|
||||
beforeEach(() => {
|
||||
authorizationService.isAuthorized = createSpy('isAuthorized').and.callFake((featureID: FeatureID) => {
|
||||
return observableOf(featureID === FeatureID.CanManageGroups);
|
||||
});
|
||||
});
|
||||
|
||||
beforeEach((done) => {
|
||||
resolver.createAdminMenu$().subscribe((_) => {
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should show access control section', () => {
|
||||
expect(menuService.addSection).toHaveBeenCalledWith(MenuID.ADMIN, jasmine.objectContaining({
|
||||
id: 'access_control', visible: true,
|
||||
}));
|
||||
expect(menuService.addSection).toHaveBeenCalledWith(MenuID.ADMIN, jasmine.objectContaining({
|
||||
parentID: 'access_control', visible: true,
|
||||
}));
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
@@ -1,21 +0,0 @@
|
||||
import { inject } from '@angular/core';
|
||||
import {
|
||||
ActivatedRouteSnapshot,
|
||||
ResolveFn,
|
||||
RouterStateSnapshot,
|
||||
} from '@angular/router';
|
||||
import { Observable } from 'rxjs';
|
||||
|
||||
import { MenuResolverService } from './menu-resolver.service';
|
||||
|
||||
|
||||
/**
|
||||
* Initialize all menus
|
||||
*/
|
||||
export const menuResolver: ResolveFn<boolean> = (
|
||||
route: ActivatedRouteSnapshot,
|
||||
state: RouterStateSnapshot,
|
||||
menuResolverService: MenuResolverService = inject(MenuResolverService),
|
||||
): Observable<boolean> => {
|
||||
return menuResolverService.resolve(route, state);
|
||||
};
|
@@ -1,3 +1,4 @@
|
||||
@if (hasSubSections$ | async) {
|
||||
<div class="ds-menu-item-wrapper text-md-center"
|
||||
[id]="'expandable-navbar-section-' + section.id"
|
||||
(mouseenter)="onMouseEnter($event)"
|
||||
@@ -39,3 +40,4 @@
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
@@ -16,7 +16,10 @@ import { of as observableOf } from 'rxjs';
|
||||
import { HostWindowService } from '../../shared/host-window.service';
|
||||
import { MenuService } from '../../shared/menu/menu.service';
|
||||
import { LinkMenuItemModel } from '../../shared/menu/menu-item/models/link.model';
|
||||
import { MenuSection } from '../../shared/menu/menu-section.model';
|
||||
import {
|
||||
MenuItemModels,
|
||||
MenuSection,
|
||||
} from '../../shared/menu/menu-section.model';
|
||||
import { HostWindowServiceStub } from '../../shared/testing/host-window-service.stub';
|
||||
import { MenuServiceStub } from '../../shared/testing/menu-service.stub';
|
||||
import { HoverOutsideDirective } from '../../shared/utils/hover-outside.directive';
|
||||
@@ -46,7 +49,7 @@ describe('ExpandableNavbarSectionComponent', () => {
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
spyOn(menuService, 'getSubSectionsByParentID').and.returnValue(observableOf([]));
|
||||
spyOn(menuService, 'getSubSectionsByParentID').and.returnValue(observableOf([{ id: 'test', visible: true, model: {} as MenuItemModels }]));
|
||||
|
||||
fixture = TestBed.createComponent(ExpandableNavbarSectionComponent);
|
||||
component = fixture.componentInstance;
|
||||
@@ -258,21 +261,23 @@ describe('ExpandableNavbarSectionComponent', () => {
|
||||
beforeEach(fakeAsync(() => {
|
||||
jasmine.getEnv().allowRespy(true);
|
||||
spyOn(menuService, 'getSubSectionsByParentID').and.returnValue(observableOf([
|
||||
Object.assign(new MenuSection(), {
|
||||
{
|
||||
id: 'subSection1',
|
||||
model: Object.assign(new LinkMenuItemModel(), {
|
||||
type: 'TEST_LINK',
|
||||
}),
|
||||
parentId: component.section.id,
|
||||
}),
|
||||
Object.assign(new MenuSection(), {
|
||||
visible: true,
|
||||
parentID: component.section.id,
|
||||
},
|
||||
{
|
||||
id: 'subSection2',
|
||||
model: Object.assign(new LinkMenuItemModel(), {
|
||||
type: 'TEST_LINK',
|
||||
}),
|
||||
parentId: component.section.id,
|
||||
}),
|
||||
]));
|
||||
visible: true,
|
||||
parentID: component.section.id,
|
||||
},
|
||||
] as MenuSection[]));
|
||||
component.ngOnInit();
|
||||
flush();
|
||||
fixture.detectChanges();
|
||||
@@ -321,7 +326,7 @@ describe('ExpandableNavbarSectionComponent', () => {
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
spyOn(menuService, 'getSubSectionsByParentID').and.returnValue(observableOf([]));
|
||||
spyOn(menuService, 'getSubSectionsByParentID').and.returnValue(observableOf([{ id: 'test', visible: true, model: {} as MenuItemModels }]));
|
||||
|
||||
fixture = TestBed.createComponent(ExpandableNavbarSectionComponent);
|
||||
component = fixture.componentInstance;
|
||||
|
@@ -13,9 +13,13 @@ import {
|
||||
} from '@angular/core';
|
||||
import { RouterLinkActive } from '@angular/router';
|
||||
import { Observable } from 'rxjs';
|
||||
import { first } from 'rxjs/operators';
|
||||
import {
|
||||
first,
|
||||
map,
|
||||
} from 'rxjs/operators';
|
||||
|
||||
import { slide } from '../../shared/animations/slide';
|
||||
import { isNotEmpty } from '../../shared/empty.util';
|
||||
import { HostWindowService } from '../../shared/host-window.service';
|
||||
import { MenuService } from '../../shared/menu/menu.service';
|
||||
import { MenuID } from '../../shared/menu/menu-id.model';
|
||||
@@ -78,6 +82,11 @@ export class ExpandableNavbarSectionComponent extends NavbarSectionComponent imp
|
||||
*/
|
||||
private dropdownItems: NodeListOf<HTMLElement>;
|
||||
|
||||
/**
|
||||
* Emits true when the top section has subsections, else emits false
|
||||
*/
|
||||
hasSubSections$: Observable<boolean>;
|
||||
|
||||
@HostListener('window:resize', ['$event'])
|
||||
onResize() {
|
||||
this.isMobile$.pipe(
|
||||
@@ -104,6 +113,9 @@ export class ExpandableNavbarSectionComponent extends NavbarSectionComponent imp
|
||||
|
||||
ngOnInit() {
|
||||
super.ngOnInit();
|
||||
this.hasSubSections$ = this.subSections$.pipe(
|
||||
map((subSections) => isNotEmpty(subSections)),
|
||||
);
|
||||
this.subs.push(this.active$.subscribe((active: boolean) => {
|
||||
if (active === true) {
|
||||
this.addArrowEventListeners = true;
|
||||
|
@@ -11,7 +11,8 @@ import {
|
||||
|
||||
import { MenuService } from '../../shared/menu/menu.service';
|
||||
import { MenuID } from '../../shared/menu/menu-id.model';
|
||||
import { MenuSectionComponent } from '../../shared/menu/menu-section/menu-section.component';
|
||||
import { MenuSection } from '../../shared/menu/menu-section.model';
|
||||
import { AbstractMenuSectionComponent } from '../../shared/menu/menu-section/abstract-menu-section.component';
|
||||
|
||||
/**
|
||||
* Represents a non-expandable section in the navbar
|
||||
@@ -23,17 +24,18 @@ import { MenuSectionComponent } from '../../shared/menu/menu-section/menu-sectio
|
||||
standalone: true,
|
||||
imports: [NgComponentOutlet, AsyncPipe],
|
||||
})
|
||||
export class NavbarSectionComponent extends MenuSectionComponent implements OnInit {
|
||||
export class NavbarSectionComponent extends AbstractMenuSectionComponent implements OnInit {
|
||||
/**
|
||||
* This section resides in the Public Navbar
|
||||
*/
|
||||
menuID = MenuID.PUBLIC;
|
||||
|
||||
constructor(@Inject('sectionDataProvider') menuSection,
|
||||
constructor(
|
||||
@Inject('sectionDataProvider') protected section: MenuSection,
|
||||
protected menuService: MenuService,
|
||||
protected injector: Injector,
|
||||
) {
|
||||
super(menuSection, menuService, injector);
|
||||
super(menuService, injector);
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
|
@@ -1,324 +0,0 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import {
|
||||
ActivatedRouteSnapshot,
|
||||
RouterStateSnapshot,
|
||||
} from '@angular/router';
|
||||
import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import {
|
||||
combineLatest,
|
||||
Observable,
|
||||
of as observableOf,
|
||||
} from 'rxjs';
|
||||
import {
|
||||
map,
|
||||
switchMap,
|
||||
} from 'rxjs/operators';
|
||||
|
||||
import { getDSORoute } from '../../app-routing-paths';
|
||||
import { DSpaceObjectDataService } from '../../core/data/dspace-object-data.service';
|
||||
import { AuthorizationDataService } from '../../core/data/feature-authorization/authorization-data.service';
|
||||
import { FeatureID } from '../../core/data/feature-authorization/feature-id';
|
||||
import { ResearcherProfileDataService } from '../../core/profile/researcher-profile-data.service';
|
||||
import { Collection } from '../../core/shared/collection.model';
|
||||
import { Community } from '../../core/shared/community.model';
|
||||
import { Item } from '../../core/shared/item.model';
|
||||
import {
|
||||
getFirstCompletedRemoteData,
|
||||
getRemoteDataPayload,
|
||||
} from '../../core/shared/operators';
|
||||
import { CorrectionTypeDataService } from '../../core/submission/correctiontype-data.service';
|
||||
import { URLCombiner } from '../../core/url-combiner/url-combiner';
|
||||
import {
|
||||
hasNoValue,
|
||||
hasValue,
|
||||
isNotEmpty,
|
||||
} from '../empty.util';
|
||||
import { MenuService } from '../menu/menu.service';
|
||||
import { MenuID } from '../menu/menu-id.model';
|
||||
import { LinkMenuItemModel } from '../menu/menu-item/models/link.model';
|
||||
import { OnClickMenuItemModel } from '../menu/menu-item/models/onclick.model';
|
||||
import { MenuItemType } from '../menu/menu-item-type.model';
|
||||
import { MenuSection } from '../menu/menu-section.model';
|
||||
import { NotificationsService } from '../notifications/notifications.service';
|
||||
import { SubscriptionModalComponent } from '../subscriptions/subscription-modal/subscription-modal.component';
|
||||
import { DsoVersioningModalService } from './dso-versioning-modal-service/dso-versioning-modal.service';
|
||||
import {
|
||||
DsoWithdrawnReinstateModalService,
|
||||
REQUEST_REINSTATE,
|
||||
REQUEST_WITHDRAWN,
|
||||
} from './dso-withdrawn-reinstate-service/dso-withdrawn-reinstate-modal.service';
|
||||
|
||||
/**
|
||||
* Creates the menus for the dspace object pages
|
||||
*/
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class DSOEditMenuResolverService {
|
||||
|
||||
constructor(
|
||||
protected dSpaceObjectDataService: DSpaceObjectDataService,
|
||||
protected menuService: MenuService,
|
||||
protected authorizationService: AuthorizationDataService,
|
||||
protected modalService: NgbModal,
|
||||
protected dsoVersioningModalService: DsoVersioningModalService,
|
||||
protected researcherProfileService: ResearcherProfileDataService,
|
||||
protected notificationsService: NotificationsService,
|
||||
protected translate: TranslateService,
|
||||
protected dsoWithdrawnReinstateModalService: DsoWithdrawnReinstateModalService,
|
||||
private correctionTypeDataService: CorrectionTypeDataService,
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialise all dspace object related menus
|
||||
*/
|
||||
resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<{ [key: string]: MenuSection[] }> {
|
||||
let id = route.params.id;
|
||||
if (hasNoValue(id) && hasValue(route.queryParams.scope)) {
|
||||
id = route.queryParams.scope;
|
||||
}
|
||||
if (hasNoValue(id)) {
|
||||
// If there's no ID, we're not on a DSO homepage, so pass on any pre-existing menu route data
|
||||
return observableOf({ ...route.data?.menu });
|
||||
} else {
|
||||
return this.dSpaceObjectDataService.findById(id, true, false).pipe(
|
||||
getFirstCompletedRemoteData(),
|
||||
switchMap((dsoRD) => {
|
||||
if (dsoRD.hasSucceeded) {
|
||||
const dso = dsoRD.payload;
|
||||
return combineLatest(this.getDsoMenus(dso, route, state)).pipe(
|
||||
// Menu sections are retrieved as an array of arrays and flattened into a single array
|
||||
map((combinedMenus) => [].concat.apply([], combinedMenus)),
|
||||
map((menus) => this.addDsoUuidToMenuIDs(menus, dso)),
|
||||
map((menus) => {
|
||||
return {
|
||||
...route.data?.menu,
|
||||
[MenuID.DSO_EDIT]: menus,
|
||||
};
|
||||
}),
|
||||
);
|
||||
} else {
|
||||
return observableOf({ ...route.data?.menu });
|
||||
}
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Return all the menus for a dso based on the route and state
|
||||
*/
|
||||
getDsoMenus(dso, route, state): Observable<MenuSection[]>[] {
|
||||
return [
|
||||
this.getItemMenu(dso),
|
||||
this.getComColMenu(dso),
|
||||
this.getCommonMenu(dso, state),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the common menus between all dspace objects
|
||||
*/
|
||||
protected getCommonMenu(dso, state): Observable<MenuSection[]> {
|
||||
return combineLatest([
|
||||
this.authorizationService.isAuthorized(FeatureID.CanEditMetadata, dso.self),
|
||||
]).pipe(
|
||||
map(([canEditItem]) => {
|
||||
return [
|
||||
{
|
||||
id: 'edit-dso',
|
||||
active: false,
|
||||
visible: canEditItem,
|
||||
model: {
|
||||
type: MenuItemType.LINK,
|
||||
text: this.getDsoType(dso) + '.page.edit',
|
||||
link: new URLCombiner(getDSORoute(dso), 'edit', 'metadata').toString(),
|
||||
} as LinkMenuItemModel,
|
||||
icon: 'pencil-alt',
|
||||
index: 2,
|
||||
},
|
||||
];
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get item specific menus
|
||||
*/
|
||||
protected getItemMenu(dso): Observable<MenuSection[]> {
|
||||
if (dso instanceof Item) {
|
||||
return combineLatest([
|
||||
this.authorizationService.isAuthorized(FeatureID.CanCreateVersion, dso.self),
|
||||
this.dsoVersioningModalService.isNewVersionButtonDisabled(dso),
|
||||
this.dsoVersioningModalService.getVersioningTooltipMessage(dso, 'item.page.version.hasDraft', 'item.page.version.create'),
|
||||
this.authorizationService.isAuthorized(FeatureID.CanSynchronizeWithORCID, dso.self),
|
||||
this.authorizationService.isAuthorized(FeatureID.CanClaimItem, dso.self),
|
||||
this.correctionTypeDataService.findByItem(dso.uuid, true).pipe(
|
||||
getFirstCompletedRemoteData(),
|
||||
getRemoteDataPayload()),
|
||||
]).pipe(
|
||||
map(([canCreateVersion, disableVersioning, versionTooltip, canSynchronizeWithOrcid, canClaimItem, correction]) => {
|
||||
const isPerson = this.getDsoType(dso) === 'person';
|
||||
return [
|
||||
{
|
||||
id: 'orcid-dso',
|
||||
active: false,
|
||||
visible: isPerson && canSynchronizeWithOrcid,
|
||||
model: {
|
||||
type: MenuItemType.LINK,
|
||||
text: 'item.page.orcid.tooltip',
|
||||
link: new URLCombiner(getDSORoute(dso), 'orcid').toString(),
|
||||
} as LinkMenuItemModel,
|
||||
icon: 'orcid fab fa-lg',
|
||||
index: 0,
|
||||
},
|
||||
{
|
||||
id: 'version-dso',
|
||||
active: false,
|
||||
visible: canCreateVersion,
|
||||
model: {
|
||||
type: MenuItemType.ONCLICK,
|
||||
text: versionTooltip,
|
||||
disabled: disableVersioning,
|
||||
function: () => {
|
||||
this.dsoVersioningModalService.openCreateVersionModal(dso);
|
||||
},
|
||||
} as OnClickMenuItemModel,
|
||||
icon: 'code-branch',
|
||||
index: 1,
|
||||
},
|
||||
{
|
||||
id: 'claim-dso',
|
||||
active: false,
|
||||
visible: isPerson && canClaimItem,
|
||||
model: {
|
||||
type: MenuItemType.ONCLICK,
|
||||
text: 'item.page.claim.button',
|
||||
function: () => {
|
||||
this.claimResearcher(dso);
|
||||
},
|
||||
} as OnClickMenuItemModel,
|
||||
icon: 'hand-paper',
|
||||
index: 3,
|
||||
},
|
||||
{
|
||||
id: 'withdrawn-item',
|
||||
active: false,
|
||||
visible: dso.isArchived && correction?.page.some((c) => c.topic === REQUEST_WITHDRAWN),
|
||||
model: {
|
||||
type: MenuItemType.ONCLICK,
|
||||
text:'item.page.withdrawn',
|
||||
function: () => {
|
||||
this.dsoWithdrawnReinstateModalService.openCreateWithdrawnReinstateModal(dso, 'request-withdrawn', dso.isArchived);
|
||||
},
|
||||
} as OnClickMenuItemModel,
|
||||
icon: 'eye-slash',
|
||||
index: 4,
|
||||
},
|
||||
{
|
||||
id: 'reinstate-item',
|
||||
active: false,
|
||||
visible: dso.isWithdrawn && correction?.page.some((c) => c.topic === REQUEST_REINSTATE),
|
||||
model: {
|
||||
type: MenuItemType.ONCLICK,
|
||||
text:'item.page.reinstate',
|
||||
function: () => {
|
||||
this.dsoWithdrawnReinstateModalService.openCreateWithdrawnReinstateModal(dso, 'request-reinstate', dso.isArchived);
|
||||
},
|
||||
} as OnClickMenuItemModel,
|
||||
icon: 'eye',
|
||||
index: 5,
|
||||
},
|
||||
];
|
||||
}),
|
||||
);
|
||||
} else {
|
||||
return observableOf([]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Community/Collection-specific menus
|
||||
*/
|
||||
protected getComColMenu(dso): Observable<MenuSection[]> {
|
||||
if (dso instanceof Community || dso instanceof Collection) {
|
||||
return combineLatest([
|
||||
this.authorizationService.isAuthorized(FeatureID.CanSubscribe, dso.self),
|
||||
]).pipe(
|
||||
map(([canSubscribe]) => {
|
||||
return [
|
||||
{
|
||||
id: 'subscribe',
|
||||
active: false,
|
||||
visible: canSubscribe,
|
||||
model: {
|
||||
type: MenuItemType.ONCLICK,
|
||||
text: 'subscriptions.tooltip',
|
||||
function: () => {
|
||||
const modalRef = this.modalService.open(SubscriptionModalComponent);
|
||||
modalRef.componentInstance.dso = dso;
|
||||
},
|
||||
} as OnClickMenuItemModel,
|
||||
icon: 'bell',
|
||||
index: 4,
|
||||
},
|
||||
];
|
||||
}),
|
||||
);
|
||||
} else {
|
||||
return observableOf([]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Claim a researcher by creating a profile
|
||||
* Shows notifications and/or hides the menu section on success/error
|
||||
*/
|
||||
protected claimResearcher(dso) {
|
||||
this.researcherProfileService.createFromExternalSourceAndReturnRelatedItemId(dso.self)
|
||||
.subscribe((id: string) => {
|
||||
if (isNotEmpty(id)) {
|
||||
this.notificationsService.success(this.translate.get('researcherprofile.success.claim.title'),
|
||||
this.translate.get('researcherprofile.success.claim.body'));
|
||||
this.authorizationService.invalidateAuthorizationsRequestCache();
|
||||
this.menuService.hideMenuSection(MenuID.DSO_EDIT, 'claim-dso-' + dso.uuid);
|
||||
} else {
|
||||
this.notificationsService.error(
|
||||
this.translate.get('researcherprofile.error.claim.title'),
|
||||
this.translate.get('researcherprofile.error.claim.body'));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the dso or entity type for an object to be used in generic messages
|
||||
*/
|
||||
protected getDsoType(dso) {
|
||||
const renderType = dso.getRenderTypes()[0];
|
||||
if (typeof renderType === 'string' || renderType instanceof String) {
|
||||
return renderType.toLowerCase();
|
||||
} else {
|
||||
return dso.type.toString().toLowerCase();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the dso uuid to all provided menu ids and parent ids
|
||||
*/
|
||||
protected addDsoUuidToMenuIDs(menus, dso) {
|
||||
return menus.map((menu) => {
|
||||
Object.assign(menu, {
|
||||
id: menu.id + '-' + dso.uuid,
|
||||
});
|
||||
if (hasValue(menu.parentID)) {
|
||||
Object.assign(menu, {
|
||||
parentID: menu.parentID + '-' + dso.uuid,
|
||||
});
|
||||
}
|
||||
return menu;
|
||||
});
|
||||
}
|
||||
|
||||
}
|
@@ -1,436 +0,0 @@
|
||||
import { NO_ERRORS_SCHEMA } from '@angular/core';
|
||||
import {
|
||||
TestBed,
|
||||
waitForAsync,
|
||||
} from '@angular/core/testing';
|
||||
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
|
||||
import { RouterTestingModule } from '@angular/router/testing';
|
||||
import {
|
||||
NgbModal,
|
||||
NgbModalRef,
|
||||
} from '@ng-bootstrap/ng-bootstrap';
|
||||
import {
|
||||
TranslateModule,
|
||||
TranslateService,
|
||||
} from '@ngx-translate/core';
|
||||
import flatten from 'lodash/flatten';
|
||||
import {
|
||||
combineLatest,
|
||||
map,
|
||||
of as observableOf,
|
||||
} from 'rxjs';
|
||||
import { CorrectionTypeDataService } from 'src/app/core/submission/correctiontype-data.service';
|
||||
|
||||
import { AdminSidebarComponent } from '../../admin/admin-sidebar/admin-sidebar.component';
|
||||
import { DSpaceObjectDataService } from '../../core/data/dspace-object-data.service';
|
||||
import { AuthorizationDataService } from '../../core/data/feature-authorization/authorization-data.service';
|
||||
import { ResearcherProfileDataService } from '../../core/profile/researcher-profile-data.service';
|
||||
import { Collection } from '../../core/shared/collection.model';
|
||||
import { Community } from '../../core/shared/community.model';
|
||||
import { DSpaceObject } from '../../core/shared/dspace-object.model';
|
||||
import { Item } from '../../core/shared/item.model';
|
||||
import { MenuService } from '../menu/menu.service';
|
||||
import { MenuID } from '../menu/menu-id.model';
|
||||
import { LinkMenuItemModel } from '../menu/menu-item/models/link.model';
|
||||
import { MenuItemType } from '../menu/menu-item-type.model';
|
||||
import { NotificationsService } from '../notifications/notifications.service';
|
||||
import {
|
||||
createFailedRemoteDataObject$,
|
||||
createSuccessfulRemoteDataObject$,
|
||||
} from '../remote-data.utils';
|
||||
import { MenuServiceStub } from '../testing/menu-service.stub';
|
||||
import { createPaginatedList } from '../testing/utils.test';
|
||||
import { DSOEditMenuResolverService } from './dso-edit-menu-resolver.service';
|
||||
import { DsoVersioningModalService } from './dso-versioning-modal-service/dso-versioning-modal.service';
|
||||
import { DsoWithdrawnReinstateModalService } from './dso-withdrawn-reinstate-service/dso-withdrawn-reinstate-modal.service';
|
||||
|
||||
describe('dsoEditMenuResolver', () => {
|
||||
|
||||
const MENU_STATE = {
|
||||
id: 'some menu',
|
||||
};
|
||||
|
||||
let resolver: DSOEditMenuResolverService;
|
||||
|
||||
let dSpaceObjectDataService;
|
||||
let menuService;
|
||||
let authorizationService;
|
||||
let dsoVersioningModalService;
|
||||
let researcherProfileService;
|
||||
let notificationsService;
|
||||
let translate;
|
||||
let mockNgbModal;
|
||||
let dsoWithdrawnReinstateModalService;
|
||||
let correctionsDataService;
|
||||
|
||||
const dsoRoute = (dso: DSpaceObject) => {
|
||||
return {
|
||||
data: {
|
||||
menu: {
|
||||
'statistics': [{
|
||||
id: 'statistics-dummy-1',
|
||||
active: false,
|
||||
visible: true,
|
||||
model: null,
|
||||
}],
|
||||
},
|
||||
},
|
||||
params: { id: dso.uuid },
|
||||
};
|
||||
};
|
||||
|
||||
const state = {
|
||||
url: 'test-url',
|
||||
};
|
||||
|
||||
const testCommunity: Community = Object.assign(new Community(), {
|
||||
uuid: 'test-community-uuid',
|
||||
type: 'community',
|
||||
_links: { self: { href: 'self-link' } },
|
||||
});
|
||||
const testCollection: Collection = Object.assign(new Collection(), {
|
||||
uuid: 'test-collection-uuid',
|
||||
type: 'collection',
|
||||
_links: { self: { href: 'self-link' } },
|
||||
});
|
||||
const testItem: Item = Object.assign(new Item(), {
|
||||
uuid: 'test-item-uuid',
|
||||
type: 'item',
|
||||
_links: { self: { href: 'self-link' } },
|
||||
});
|
||||
|
||||
let testObject: DSpaceObject;
|
||||
let route;
|
||||
|
||||
const dummySections1 = [{
|
||||
id: 'dummy-1',
|
||||
active: false,
|
||||
visible: true,
|
||||
model: null,
|
||||
},
|
||||
{
|
||||
id: 'dummy-2',
|
||||
active: false,
|
||||
visible: true,
|
||||
model: null,
|
||||
}];
|
||||
|
||||
const dummySections2 = [{
|
||||
id: 'dummy-3',
|
||||
active: false,
|
||||
visible: true,
|
||||
model: null,
|
||||
},
|
||||
{
|
||||
id: 'dummy-4',
|
||||
active: false,
|
||||
visible: true,
|
||||
model: null,
|
||||
},
|
||||
{
|
||||
id: 'dummy-5',
|
||||
active: false,
|
||||
visible: true,
|
||||
model: null,
|
||||
}];
|
||||
|
||||
beforeEach(waitForAsync(() => {
|
||||
// test with Items unless specified otherwise
|
||||
testObject = testItem;
|
||||
route = dsoRoute(testItem);
|
||||
|
||||
menuService = new MenuServiceStub();
|
||||
spyOn(menuService, 'getMenu').and.returnValue(observableOf(MENU_STATE));
|
||||
|
||||
dSpaceObjectDataService = jasmine.createSpyObj('dSpaceObjectDataService', {
|
||||
findById: createSuccessfulRemoteDataObject$(testObject),
|
||||
});
|
||||
authorizationService = jasmine.createSpyObj('authorizationService', {
|
||||
isAuthorized: observableOf(true),
|
||||
});
|
||||
dsoVersioningModalService = jasmine.createSpyObj('dsoVersioningModalService', {
|
||||
isNewVersionButtonDisabled: observableOf(false),
|
||||
getVersioningTooltipMessage: observableOf('message'),
|
||||
openCreateVersionModal: {},
|
||||
});
|
||||
researcherProfileService = jasmine.createSpyObj('researcherProfileService', {
|
||||
createFromExternalSourceAndReturnRelatedItemId: observableOf('mock-id'),
|
||||
});
|
||||
translate = jasmine.createSpyObj('translate', {
|
||||
get: observableOf('translated-message'),
|
||||
});
|
||||
notificationsService = jasmine.createSpyObj('notificationsService', {
|
||||
success: {},
|
||||
error: {},
|
||||
});
|
||||
mockNgbModal = {
|
||||
open: jasmine.createSpy('open').and.returnValue(
|
||||
{ componentInstance: {}, closed: observableOf({}) } as NgbModalRef,
|
||||
),
|
||||
};
|
||||
|
||||
dsoWithdrawnReinstateModalService = jasmine.createSpyObj('dsoWithdrawnReinstateModalService', {
|
||||
openCreateWithdrawnReinstateModal: {},
|
||||
});
|
||||
|
||||
correctionsDataService = jasmine.createSpyObj('correctionsDataService', {
|
||||
findByItem: createSuccessfulRemoteDataObject$(createPaginatedList([])),
|
||||
});
|
||||
|
||||
TestBed.configureTestingModule({
|
||||
imports: [TranslateModule.forRoot(), NoopAnimationsModule, RouterTestingModule, AdminSidebarComponent],
|
||||
providers: [
|
||||
{ provide: DSpaceObjectDataService, useValue: dSpaceObjectDataService },
|
||||
{ provide: MenuService, useValue: menuService },
|
||||
{ provide: AuthorizationDataService, useValue: authorizationService },
|
||||
{ provide: DsoVersioningModalService, useValue: dsoVersioningModalService },
|
||||
{ provide: ResearcherProfileDataService, useValue: researcherProfileService },
|
||||
{ provide: TranslateService, useValue: translate },
|
||||
{ provide: NotificationsService, useValue: notificationsService },
|
||||
{ provide: DsoWithdrawnReinstateModalService, useValue: dsoWithdrawnReinstateModalService },
|
||||
{ provide: CorrectionTypeDataService, useValue: correctionsDataService },
|
||||
{ provide: NgbModal, useValue: mockNgbModal },
|
||||
DSOEditMenuResolverService,
|
||||
],
|
||||
schemas: [NO_ERRORS_SCHEMA],
|
||||
});
|
||||
resolver = TestBed.inject(DSOEditMenuResolverService);
|
||||
|
||||
spyOn(menuService, 'addSection');
|
||||
}));
|
||||
|
||||
it('should be created', () => {
|
||||
expect(resolver).toBeTruthy();
|
||||
});
|
||||
|
||||
describe('resolve', () => {
|
||||
it('should create all menus when a dso is found based on the route id param', (done) => {
|
||||
spyOn(resolver, 'getDsoMenus').and.returnValue(
|
||||
[observableOf(dummySections1), observableOf(dummySections2)],
|
||||
);
|
||||
resolver.resolve(route as any, null).subscribe(resolved => {
|
||||
expect(resolved).toEqual(
|
||||
{
|
||||
...route.data.menu,
|
||||
[MenuID.DSO_EDIT]: [
|
||||
...dummySections1.map((menu) => Object.assign(menu, { id: menu.id + '-test-item-uuid' })),
|
||||
...dummySections2.map((menu) => Object.assign(menu, { id: menu.id + '-test-item-uuid' })),
|
||||
],
|
||||
},
|
||||
);
|
||||
expect(dSpaceObjectDataService.findById).toHaveBeenCalledWith('test-item-uuid', true, false);
|
||||
expect(resolver.getDsoMenus).toHaveBeenCalled();
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should create all menus when a dso is found based on the route scope query param when no id param is present', (done) => {
|
||||
spyOn(resolver, 'getDsoMenus').and.returnValue(
|
||||
[observableOf(dummySections1), observableOf(dummySections2)],
|
||||
);
|
||||
const routeWithScope = {
|
||||
data: {
|
||||
menu: {
|
||||
'statistics': [{
|
||||
id: 'statistics-dummy-1',
|
||||
active: false,
|
||||
visible: true,
|
||||
model: null,
|
||||
}],
|
||||
},
|
||||
},
|
||||
params: {},
|
||||
queryParams: { scope: 'test-scope-uuid' },
|
||||
};
|
||||
|
||||
resolver.resolve(routeWithScope as any, null).subscribe(resolved => {
|
||||
expect(resolved).toEqual(
|
||||
{
|
||||
...route.data.menu,
|
||||
[MenuID.DSO_EDIT]: [
|
||||
...dummySections1.map((menu) => Object.assign(menu, { id: menu.id + '-test-scope-uuid' })),
|
||||
...dummySections2.map((menu) => Object.assign(menu, { id: menu.id + '-test-scope-uuid' })),
|
||||
],
|
||||
},
|
||||
);
|
||||
expect(dSpaceObjectDataService.findById).toHaveBeenCalledWith('test-scope-uuid', true, false);
|
||||
expect(resolver.getDsoMenus).toHaveBeenCalled();
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should return the statistics menu when no dso is found', (done) => {
|
||||
(dSpaceObjectDataService.findById as jasmine.Spy).and.returnValue(createFailedRemoteDataObject$());
|
||||
|
||||
resolver.resolve(route as any, null).subscribe(resolved => {
|
||||
expect(resolved).toEqual(
|
||||
{
|
||||
...route.data.menu,
|
||||
},
|
||||
);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('getDsoMenus', () => {
|
||||
describe('for Communities', () => {
|
||||
beforeEach(() => {
|
||||
testObject = testCommunity;
|
||||
dSpaceObjectDataService.findById.and.returnValue(createSuccessfulRemoteDataObject$(testCommunity));
|
||||
route = dsoRoute(testCommunity);
|
||||
});
|
||||
|
||||
it('should not return Item-specific entries', (done) => {
|
||||
const result = resolver.getDsoMenus(testObject, route, state);
|
||||
combineLatest(result).pipe(map(flatten)).subscribe((menu) => {
|
||||
const orcidEntry = menu.find(entry => entry.id === 'orcid-dso');
|
||||
expect(orcidEntry).toBeFalsy();
|
||||
|
||||
const versionEntry = menu.find(entry => entry.id === 'version-dso');
|
||||
expect(versionEntry).toBeFalsy();
|
||||
|
||||
const claimEntry = menu.find(entry => entry.id === 'claim-dso');
|
||||
expect(claimEntry).toBeFalsy();
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should return Community/Collection-specific entries', (done) => {
|
||||
const result = resolver.getDsoMenus(testObject, route, state);
|
||||
combineLatest(result).pipe(map(flatten)).subscribe((menu) => {
|
||||
const subscribeEntry = menu.find(entry => entry.id === 'subscribe');
|
||||
expect(subscribeEntry).toBeTruthy();
|
||||
expect(subscribeEntry.active).toBeFalse();
|
||||
expect(subscribeEntry.visible).toBeTrue();
|
||||
expect(subscribeEntry.model.type).toEqual(MenuItemType.ONCLICK);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should return as third part the common list ', (done) => {
|
||||
const result = resolver.getDsoMenus(testObject, route, state);
|
||||
combineLatest(result).pipe(map(flatten)).subscribe((menu) => {
|
||||
const editEntry = menu.find(entry => entry.id === 'edit-dso');
|
||||
expect(editEntry).toBeTruthy();
|
||||
expect(editEntry.active).toBeFalse();
|
||||
expect(editEntry.visible).toBeTrue();
|
||||
expect(editEntry.model.type).toEqual(MenuItemType.LINK);
|
||||
expect((editEntry.model as LinkMenuItemModel).link).toEqual(
|
||||
'/communities/test-community-uuid/edit/metadata',
|
||||
);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('for Collections', () => {
|
||||
beforeEach(() => {
|
||||
testObject = testCollection;
|
||||
dSpaceObjectDataService.findById.and.returnValue(createSuccessfulRemoteDataObject$(testCollection));
|
||||
route = dsoRoute(testCollection);
|
||||
});
|
||||
|
||||
it('should not return Item-specific entries', (done) => {
|
||||
const result = resolver.getDsoMenus(testObject, route, state);
|
||||
combineLatest(result).pipe(map(flatten)).subscribe((menu) => {
|
||||
const orcidEntry = menu.find(entry => entry.id === 'orcid-dso');
|
||||
expect(orcidEntry).toBeFalsy();
|
||||
|
||||
const versionEntry = menu.find(entry => entry.id === 'version-dso');
|
||||
expect(versionEntry).toBeFalsy();
|
||||
|
||||
const claimEntry = menu.find(entry => entry.id === 'claim-dso');
|
||||
expect(claimEntry).toBeFalsy();
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should return Community/Collection-specific entries', (done) => {
|
||||
const result = resolver.getDsoMenus(testObject, route, state);
|
||||
combineLatest(result).pipe(map(flatten)).subscribe((menu) => {
|
||||
const subscribeEntry = menu.find(entry => entry.id === 'subscribe');
|
||||
expect(subscribeEntry).toBeTruthy();
|
||||
expect(subscribeEntry.active).toBeFalse();
|
||||
expect(subscribeEntry.visible).toBeTrue();
|
||||
expect(subscribeEntry.model.type).toEqual(MenuItemType.ONCLICK);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should return as third part the common list ', (done) => {
|
||||
const result = resolver.getDsoMenus(testObject, route, state);
|
||||
combineLatest(result).pipe(map(flatten)).subscribe((menu) => {
|
||||
const editEntry = menu.find(entry => entry.id === 'edit-dso');
|
||||
expect(editEntry).toBeTruthy();
|
||||
expect(editEntry.active).toBeFalse();
|
||||
expect(editEntry.visible).toBeTrue();
|
||||
expect(editEntry.model.type).toEqual(MenuItemType.LINK);
|
||||
expect((editEntry.model as LinkMenuItemModel).link).toEqual(
|
||||
'/collections/test-collection-uuid/edit/metadata',
|
||||
);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('for Items', () => {
|
||||
beforeEach(() => {
|
||||
testObject = testItem;
|
||||
dSpaceObjectDataService.findById.and.returnValue(createSuccessfulRemoteDataObject$(testItem));
|
||||
route = dsoRoute(testItem);
|
||||
});
|
||||
|
||||
it('should return Item-specific entries', (done: DoneFn) => {
|
||||
const result = resolver.getDsoMenus(testObject, route, state);
|
||||
combineLatest(result).pipe(map(flatten)).subscribe((menu) => {
|
||||
const orcidEntry = menu.find(entry => entry.id === 'orcid-dso');
|
||||
expect(orcidEntry).toBeTruthy();
|
||||
expect(orcidEntry.active).toBeFalse();
|
||||
expect(orcidEntry.visible).toBeFalse();
|
||||
expect(orcidEntry.model.type).toEqual(MenuItemType.LINK);
|
||||
|
||||
const versionEntry = menu.find(entry => entry.id === 'version-dso');
|
||||
expect(versionEntry).toBeTruthy();
|
||||
expect(versionEntry.active).toBeFalse();
|
||||
expect(versionEntry.visible).toBeTrue();
|
||||
expect(versionEntry.model.type).toEqual(MenuItemType.ONCLICK);
|
||||
expect(versionEntry.model.disabled).toBeFalse();
|
||||
|
||||
const claimEntry = menu.find(entry => entry.id === 'claim-dso');
|
||||
expect(claimEntry).toBeTruthy();
|
||||
expect(claimEntry.active).toBeFalse();
|
||||
expect(claimEntry.visible).toBeFalse();
|
||||
expect(claimEntry.model.type).toEqual(MenuItemType.ONCLICK);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should not return Community/Collection-specific entries', (done: DoneFn) => {
|
||||
const result = resolver.getDsoMenus(testObject, route, state);
|
||||
combineLatest(result).pipe(map(flatten)).subscribe((menu) => {
|
||||
const subscribeEntry = menu.find(entry => entry.id === 'subscribe');
|
||||
expect(subscribeEntry).toBeFalsy();
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should return as third part the common list ', (done: DoneFn) => {
|
||||
const result = resolver.getDsoMenus(testObject, route, state);
|
||||
combineLatest(result).pipe(map(flatten)).subscribe((menu) => {
|
||||
const editEntry = menu.find(entry => entry.id === 'edit-dso');
|
||||
expect(editEntry).toBeTruthy();
|
||||
expect(editEntry.active).toBeFalse();
|
||||
expect(editEntry.visible).toBeTrue();
|
||||
expect(editEntry.model.type).toEqual(MenuItemType.LINK);
|
||||
expect((editEntry.model as LinkMenuItemModel).link).toEqual(
|
||||
'/items/test-item-uuid/edit/metadata',
|
||||
);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
@@ -1,21 +0,0 @@
|
||||
import { inject } from '@angular/core';
|
||||
import {
|
||||
ActivatedRouteSnapshot,
|
||||
ResolveFn,
|
||||
RouterStateSnapshot,
|
||||
} from '@angular/router';
|
||||
import { Observable } from 'rxjs';
|
||||
|
||||
import { MenuSection } from '../menu/menu-section.model';
|
||||
import { DSOEditMenuResolverService } from './dso-edit-menu-resolver.service';
|
||||
|
||||
/**
|
||||
* Initialise all dspace object related menus
|
||||
*/
|
||||
export const dsoEditMenuResolver: ResolveFn<{ [key: string]: MenuSection[] }> = (
|
||||
route: ActivatedRouteSnapshot,
|
||||
state: RouterStateSnapshot,
|
||||
menuResolverService: DSOEditMenuResolverService = inject(DSOEditMenuResolverService),
|
||||
): Observable<{ [key: string]: MenuSection[] }> => {
|
||||
return menuResolverService.resolve(route, state);
|
||||
};
|
@@ -1,18 +1,19 @@
|
||||
@if (hasSubSections$ | async) {
|
||||
<div class="dso-button-menu mb-1" ngbDropdown container="body" placement="bottom-right">
|
||||
<div class="d-flex flex-row flex-nowrap"
|
||||
[ngbTooltip]="itemModel.text | translate" container="body">
|
||||
<button [attr.aria-label]="itemModel.text | translate" [title]="itemModel.text | translate" class="btn btn-dark btn-sm" ngbDropdownToggle [dsBtnDisabled]="section.model?.disabled">
|
||||
<button [attr.aria-label]="itemModel.text | translate" [title]="itemModel.text | translate" class="btn btn-dark btn-sm" ngbDropdownToggle [dsBtnDisabled]="section.model?.disabled" role="menuitem" tabindex="0">
|
||||
<i class="fas fa-{{section.icon}} fa-fw"></i>
|
||||
</button>
|
||||
<ul ngbDropdownMenu class="dso-edit-menu-dropdown">
|
||||
<ul ngbDropdownMenu class="dso-edit-menu-dropdown p-1" role="menu">
|
||||
@for (subSection of (subSections$ | async); track subSection) {
|
||||
<li class="nav-item nav-link d-flex flex-row">
|
||||
<li class="nav-item nav-link d-flex flex-row p-2" role="presentation">
|
||||
@if (renderIcons$ | async) {
|
||||
<div class="me-2">
|
||||
@if (subSection.icon) {
|
||||
<i class="fas fa-{{subSection.icon}} fa-fw"></i>
|
||||
<i class="fas fa-{{subSection.icon}} fa-fw" aria-hidden="true"></i>
|
||||
} @else {
|
||||
<i class="fas fa-fw"></i>
|
||||
<i class="fas fa-fw" aria-hidden="true"></i>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
@@ -24,6 +25,7 @@
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
@@ -4,8 +4,19 @@
|
||||
|
||||
.dso-button-menu {
|
||||
.dropdown-toggle::after {
|
||||
display: none;
|
||||
content: '';
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-style: solid;
|
||||
border-width: 12px 12px 0 0;
|
||||
border-color: transparent #627a91 transparent transparent;
|
||||
border-bottom-right-radius: var(--bs-btn-border-radius-sm);
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
position: absolute;
|
||||
overflow: hidden;
|
||||
}
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
ul.dropdown-menu {
|
||||
|
@@ -8,9 +8,10 @@ import { By } from '@angular/platform-browser';
|
||||
import { Router } from '@angular/router';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { of as observableOf } from 'rxjs';
|
||||
import { MenuItemType } from 'src/app/shared/menu/menu-item-type.model';
|
||||
|
||||
import { MenuService } from '../../../menu/menu.service';
|
||||
import { MenuItemType } from '../../../menu/menu-item-type.model';
|
||||
import { MenuItemModels } from '../../../menu/menu-section.model';
|
||||
import { CSSVariableService } from '../../../sass-helper/css-variable.service';
|
||||
import { CSSVariableServiceStub } from '../../../testing/css-variable-service.stub';
|
||||
import { MenuServiceStub } from '../../../testing/menu-service.stub';
|
||||
@@ -35,6 +36,42 @@ describe('DsoEditMenuExpandableSectionComponent', () => {
|
||||
icon: iconString,
|
||||
};
|
||||
|
||||
describe('when there are subsections', () => {
|
||||
beforeEach(waitForAsync(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [TranslateModule.forRoot(), DsoEditMenuExpandableSectionComponent, TestComponent],
|
||||
providers: [
|
||||
{ provide: 'sectionDataProvider', useValue: dummySection },
|
||||
{ provide: MenuService, useValue: menuService },
|
||||
{ provide: CSSVariableService, useClass: CSSVariableServiceStub },
|
||||
{ provide: Router, useValue: new RouterStub() },
|
||||
],
|
||||
}).compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
spyOn(menuService, 'getSubSectionsByParentID').and.returnValue(observableOf([{
|
||||
id: 'test',
|
||||
visible: true,
|
||||
model: {} as MenuItemModels,
|
||||
}]));
|
||||
fixture = TestBed.createComponent(DsoEditMenuExpandableSectionComponent);
|
||||
component = fixture.componentInstance;
|
||||
spyOn(component as any, 'getMenuItemComponent').and.returnValue(TestComponent);
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should show a button with the icon', () => {
|
||||
const button = fixture.debugElement.query(By.css('.btn-dark'));
|
||||
expect(button.nativeElement.innerHTML).toContain('fa-' + iconString);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when there are no subsections', () => {
|
||||
beforeEach(waitForAsync(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [TranslateModule.forRoot(), DsoEditMenuExpandableSectionComponent, TestComponent],
|
||||
@@ -59,9 +96,10 @@ describe('DsoEditMenuExpandableSectionComponent', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should show a button with the icon', () => {
|
||||
it('should now show a button', () => {
|
||||
const button = fixture.debugElement.query(By.css('.btn-dark'));
|
||||
expect(button.nativeElement.innerHTML).toContain('fa-' + iconString);
|
||||
expect(button).toBeNull();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
@@ -18,10 +18,13 @@ import { Observable } from 'rxjs';
|
||||
import { map } from 'rxjs/operators';
|
||||
import { MenuID } from 'src/app/shared/menu/menu-id.model';
|
||||
import { MenuSection } from 'src/app/shared/menu/menu-section.model';
|
||||
import { MenuSectionComponent } from 'src/app/shared/menu/menu-section/menu-section.component';
|
||||
import { AbstractMenuSectionComponent } from 'src/app/shared/menu/menu-section/abstract-menu-section.component';
|
||||
|
||||
import { BtnDisabledDirective } from '../../../btn-disabled.directive';
|
||||
import { hasValue } from '../../../empty.util';
|
||||
import {
|
||||
hasValue,
|
||||
isNotEmpty,
|
||||
} from '../../../empty.util';
|
||||
import { MenuService } from '../../../menu/menu.service';
|
||||
|
||||
/**
|
||||
@@ -34,21 +37,37 @@ import { MenuService } from '../../../menu/menu.service';
|
||||
standalone: true,
|
||||
imports: [NgbDropdownModule, NgbTooltipModule, NgComponentOutlet, TranslateModule, AsyncPipe, BtnDisabledDirective],
|
||||
})
|
||||
export class DsoEditMenuExpandableSectionComponent extends MenuSectionComponent implements OnInit {
|
||||
export class DsoEditMenuExpandableSectionComponent extends AbstractMenuSectionComponent implements OnInit {
|
||||
|
||||
/**
|
||||
* This section resides in the DSO edit menu
|
||||
*/
|
||||
menuID: MenuID = MenuID.DSO_EDIT;
|
||||
|
||||
|
||||
/**
|
||||
* The MenuItemModel of the top section
|
||||
*/
|
||||
itemModel;
|
||||
|
||||
/**
|
||||
* Emits whether one of the subsections contains an icon
|
||||
*/
|
||||
renderIcons$: Observable<boolean>;
|
||||
|
||||
/**
|
||||
* Emits true when the top section has subsections, else emits false
|
||||
*/
|
||||
hasSubSections$: Observable<boolean>;
|
||||
|
||||
constructor(
|
||||
@Inject('sectionDataProvider') menuSection: MenuSection,
|
||||
@Inject('sectionDataProvider') protected section: MenuSection,
|
||||
protected menuService: MenuService,
|
||||
protected injector: Injector,
|
||||
protected router: Router,
|
||||
) {
|
||||
super(menuSection, menuService, injector);
|
||||
this.itemModel = menuSection.model;
|
||||
super(menuService, injector);
|
||||
this.itemModel = section.model;
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
@@ -60,5 +79,10 @@ export class DsoEditMenuExpandableSectionComponent extends MenuSectionComponent
|
||||
return sections.some(section => hasValue(section.icon));
|
||||
}),
|
||||
);
|
||||
|
||||
this.hasSubSections$ = this.subSections$.pipe(
|
||||
map((subSections) => isNotEmpty(subSections)),
|
||||
);
|
||||
|
||||
}
|
||||
}
|
||||
|
@@ -142,17 +142,17 @@ describe('DsoEditMenuSectionComponent', () => {
|
||||
stopPropagation: jasmine.createSpy('stopPropagation'),
|
||||
});
|
||||
it('should call the item model function when not disabled', () => {
|
||||
spyOn(component.section.model as OnClickMenuItemModel, 'function');
|
||||
spyOn((component as any).section.model as OnClickMenuItemModel, 'function');
|
||||
component.activate(mockEvent);
|
||||
|
||||
expect((component.section.model as OnClickMenuItemModel).function).toHaveBeenCalled();
|
||||
expect(((component as any).section.model as OnClickMenuItemModel).function).toHaveBeenCalled();
|
||||
});
|
||||
it('should call not the item model function when disabled', () => {
|
||||
spyOn(component.section.model as OnClickMenuItemModel, 'function');
|
||||
spyOn((component as any).section.model as OnClickMenuItemModel, 'function');
|
||||
component.itemModel.disabled = true;
|
||||
component.activate(mockEvent);
|
||||
|
||||
expect((component.section.model as OnClickMenuItemModel).function).not.toHaveBeenCalled();
|
||||
expect(((component as any).section.model as OnClickMenuItemModel).function).not.toHaveBeenCalled();
|
||||
component.itemModel.disabled = false;
|
||||
});
|
||||
});
|
||||
|
@@ -8,7 +8,7 @@ import {
|
||||
import { RouterLink } from '@angular/router';
|
||||
import { NgbTooltipModule } from '@ng-bootstrap/ng-bootstrap';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { MenuSectionComponent } from 'src/app/shared/menu/menu-section/menu-section.component';
|
||||
import { AbstractMenuSectionComponent } from 'src/app/shared/menu/menu-section/abstract-menu-section.component';
|
||||
|
||||
import { BtnDisabledDirective } from '../../../btn-disabled.directive';
|
||||
import { isNotEmpty } from '../../../empty.util';
|
||||
@@ -26,7 +26,7 @@ import { MenuSection } from '../../../menu/menu-section.model';
|
||||
standalone: true,
|
||||
imports: [NgbTooltipModule, RouterLink, TranslateModule, BtnDisabledDirective],
|
||||
})
|
||||
export class DsoEditMenuSectionComponent extends MenuSectionComponent implements OnInit {
|
||||
export class DsoEditMenuSectionComponent extends AbstractMenuSectionComponent implements OnInit {
|
||||
|
||||
menuID: MenuID = MenuID.DSO_EDIT;
|
||||
itemModel;
|
||||
@@ -34,12 +34,12 @@ export class DsoEditMenuSectionComponent extends MenuSectionComponent implements
|
||||
canActivate: boolean;
|
||||
|
||||
constructor(
|
||||
@Inject('sectionDataProvider') menuSection: MenuSection,
|
||||
@Inject('sectionDataProvider') protected section: MenuSection,
|
||||
protected menuService: MenuService,
|
||||
protected injector: Injector,
|
||||
) {
|
||||
super(menuSection, menuService, injector);
|
||||
this.itemModel = menuSection.model;
|
||||
super(menuService, injector);
|
||||
this.itemModel = section.model;
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
|
@@ -1,4 +1,4 @@
|
||||
<div class="dso-edit-menu d-flex">
|
||||
<div class="dso-edit-menu d-flex" role="menubar">
|
||||
@for (section of (sections | async); track section) {
|
||||
<div class="ms-1">
|
||||
<ng-container
|
||||
|
@@ -15,7 +15,7 @@ import { of as observableOf } from 'rxjs';
|
||||
import { AuthService } from '../../../core/auth/auth.service';
|
||||
import { AuthorizationDataService } from '../../../core/data/feature-authorization/authorization-data.service';
|
||||
import { MenuService } from '../../menu/menu.service';
|
||||
import { MenuItemModel } from '../../menu/menu-item/models/menu-item.model';
|
||||
import { TextMenuItemModel } from '../../menu/menu-item/models/text.model';
|
||||
import { getMockThemeService } from '../../mocks/theme-service.mock';
|
||||
import { AuthServiceStub } from '../../testing/auth-service.stub';
|
||||
import { MenuServiceStub } from '../../testing/menu-service.stub';
|
||||
@@ -37,9 +37,10 @@ describe('DsoEditMenuComponent', () => {
|
||||
active: false,
|
||||
visible: true,
|
||||
model: {
|
||||
text: 'section-text',
|
||||
type: null,
|
||||
disabled: false,
|
||||
} as MenuItemModel,
|
||||
} as TextMenuItemModel,
|
||||
icon: 'pencil-alt',
|
||||
index: 1,
|
||||
};
|
||||
|
@@ -28,7 +28,7 @@ export const initialMenusState: MenusState = {
|
||||
id: MenuID.DSO_EDIT,
|
||||
collapsed: true,
|
||||
previewCollapsed: true,
|
||||
visible: false,
|
||||
visible: true,
|
||||
sections: {},
|
||||
sectionToSubsectionIndex: {},
|
||||
},
|
||||
|
@@ -6,6 +6,6 @@ import { MenuItemModel } from './menu-item.model';
|
||||
*/
|
||||
export class AltmetricMenuItemModel implements MenuItemModel {
|
||||
type = MenuItemType.ALTMETRIC;
|
||||
disabled: boolean;
|
||||
disabled?: boolean;
|
||||
url: string;
|
||||
}
|
||||
|
@@ -6,7 +6,7 @@ import { MenuItemModel } from './menu-item.model';
|
||||
*/
|
||||
export class ExternalLinkMenuItemModel implements MenuItemModel {
|
||||
type = MenuItemType.EXTERNAL;
|
||||
disabled: boolean;
|
||||
disabled?: boolean;
|
||||
text: string;
|
||||
href: string;
|
||||
}
|
||||
|
@@ -8,7 +8,7 @@ import { MenuItemModel } from './menu-item.model';
|
||||
*/
|
||||
export class LinkMenuItemModel implements MenuItemModel {
|
||||
type = MenuItemType.LINK;
|
||||
disabled: boolean;
|
||||
disabled?: boolean;
|
||||
text: string;
|
||||
link: string;
|
||||
queryParams?: Params | null;
|
||||
|
@@ -5,5 +5,5 @@ import { MenuItemType } from '../../menu-item-type.model';
|
||||
*/
|
||||
export interface MenuItemModel {
|
||||
type: MenuItemType;
|
||||
disabled: boolean;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
@@ -6,7 +6,7 @@ import { MenuItemModel } from './menu-item.model';
|
||||
*/
|
||||
export class OnClickMenuItemModel implements MenuItemModel {
|
||||
type = MenuItemType.ONCLICK;
|
||||
disabled: boolean;
|
||||
disabled?: boolean;
|
||||
text: string;
|
||||
function: () => void;
|
||||
}
|
||||
|
@@ -6,7 +6,7 @@ import { MenuItemModel } from './menu-item.model';
|
||||
*/
|
||||
export class SearchMenuItemModel implements MenuItemModel {
|
||||
type = MenuItemType.SEARCH;
|
||||
disabled: boolean;
|
||||
disabled?: boolean;
|
||||
placeholder: string;
|
||||
action: string;
|
||||
}
|
||||
|
@@ -6,6 +6,6 @@ import { MenuItemModel } from './menu-item.model';
|
||||
*/
|
||||
export class TextMenuItemModel implements MenuItemModel {
|
||||
type = MenuItemType.TEXT;
|
||||
disabled: boolean;
|
||||
disabled?: boolean;
|
||||
text: string;
|
||||
}
|
||||
|
162
src/app/shared/menu/menu-provider.model.ts
Normal file
162
src/app/shared/menu/menu-provider.model.ts
Normal file
@@ -0,0 +1,162 @@
|
||||
/**
|
||||
* The contents of this file are subject to the license and copyright
|
||||
* detailed in the LICENSE and NOTICE files at the root of the source
|
||||
* tree and available online at
|
||||
*
|
||||
* http://www.dspace.org/license/
|
||||
*/
|
||||
/* eslint-disable max-classes-per-file */
|
||||
import { Type } from '@angular/core';
|
||||
import {
|
||||
ActivatedRouteSnapshot,
|
||||
RouterStateSnapshot,
|
||||
} from '@angular/router';
|
||||
import { Observable } from 'rxjs';
|
||||
|
||||
import { MenuID } from './menu-id.model';
|
||||
import { MenuRoute } from './menu-route.model';
|
||||
import { MenuItemModels } from './menu-section.model';
|
||||
|
||||
/**
|
||||
* Partial menu section
|
||||
* This object acts like a menu section but with certain properties being optional
|
||||
*/
|
||||
export interface PartialMenuSection {
|
||||
id?: string;
|
||||
accessibilityHandle?: string;
|
||||
visible: boolean;
|
||||
model: MenuItemModels;
|
||||
parentID?: string;
|
||||
active?: boolean;
|
||||
shouldPersistOnRouteChange?: boolean;
|
||||
icon?: string;
|
||||
alwaysRenderExpandable?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Interface to represent a menu provider
|
||||
* Implementations of this provider will contain sections to be added to the menus
|
||||
*/
|
||||
export interface MenuProvider {
|
||||
shouldPersistOnRouteChange?: boolean,
|
||||
menuID?: MenuID;
|
||||
index?: number;
|
||||
|
||||
/**
|
||||
* Retrieve the sections from the provider. These sections can be route dependent.
|
||||
* @param route - The route on which the menu sections possibly depend
|
||||
* @param state - The router snapshot on which the sections possibly depend
|
||||
*/
|
||||
getSections(route?: ActivatedRouteSnapshot, state?: RouterStateSnapshot): Observable<PartialMenuSection[]>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Class to represent a Menu Provider together with additional information added through the static methods on
|
||||
* AbstractMenuProvider. This additional information is either the paths on which the sections of this provider should
|
||||
* be present or a list of child providers
|
||||
*/
|
||||
export class MenuProviderTypeWithOptions {
|
||||
providerType: Type<MenuProvider>;
|
||||
paths?: MenuRoute[];
|
||||
childProviderTypes?: (Type<MenuProvider> | MenuProviderTypeWithOptions)[];
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Abstract class to be extended when creating menu providers
|
||||
*/
|
||||
export abstract class AbstractMenuProvider implements MenuProvider {
|
||||
|
||||
/**
|
||||
* ID of the menu this provider is part of
|
||||
* This will be set to the menu ID of the menu in which it is present in the app.menus.ts file
|
||||
*/
|
||||
menuID?: MenuID;
|
||||
|
||||
/**
|
||||
* Whether the sections of this menu should be set on the
|
||||
*/
|
||||
shouldPersistOnRouteChange = true;
|
||||
|
||||
/**
|
||||
* The ID of the menu provider.
|
||||
* This will be automatically set based on the menu and the index of the provider in the list
|
||||
*/
|
||||
menuProviderId?: string;
|
||||
|
||||
/**
|
||||
* The index of the menu provider
|
||||
* This will be automatically set based on the index of the provider in the list
|
||||
*/
|
||||
index?: number;
|
||||
|
||||
/**
|
||||
* The paths on which the sections of this provider will be active
|
||||
* This will be automatically set based on the paths added based on the paths provided through the 'onRoute' static
|
||||
* method in the app.menus.ts file
|
||||
*/
|
||||
activePaths?: MenuRoute[];
|
||||
|
||||
/**
|
||||
* The ID of the parent provider of this provider.
|
||||
* This will be automatically set based on the provider that calls the 'withSubs' static method with this provider
|
||||
* in the list of arguments
|
||||
*/
|
||||
parentID?: string;
|
||||
|
||||
/**
|
||||
* When true, the sections added by this provider will be assumed to be parent sections with children
|
||||
* The sections will not be rendered when they have no visible children
|
||||
* This can be overwritten on the level of sections
|
||||
*/
|
||||
alwaysRenderExpandable? = false;
|
||||
|
||||
/**
|
||||
* When true, this provider will only add its sections on Browser Side Rendering.
|
||||
*/
|
||||
renderBrowserOnly? = false;
|
||||
|
||||
/**
|
||||
* Static method to be called from the app.menus.ts file to define paths on which this provider should the active
|
||||
* @param paths - The paths on which the sections of this provider should be active
|
||||
*/
|
||||
public static onRoute(...paths: MenuRoute[]): MenuProviderTypeWithOptions {
|
||||
if (!AbstractMenuProvider.isPrototypeOf(this)) {
|
||||
throw new Error(
|
||||
'onRoute should only be called from concrete subclasses of AbstractMenuProvider',
|
||||
);
|
||||
}
|
||||
|
||||
const providerType = this as unknown as Type<AbstractMenuProvider>;
|
||||
return { providerType: providerType, paths: paths };
|
||||
}
|
||||
|
||||
/**
|
||||
* Static method to be called from the app.menus.ts file to add sub menu providers to this top provider
|
||||
* @param childProviders - the list of sub providers that will provide subsections for this provider
|
||||
*/
|
||||
public static withSubs(childProviders: (Type<MenuProvider> | MenuProviderTypeWithOptions)[]): MenuProviderTypeWithOptions {
|
||||
if (!AbstractMenuProvider.isPrototypeOf(this)) {
|
||||
throw new Error(
|
||||
'withSubs should only be called from concrete subclasses of AbstractMenuProvider',
|
||||
);
|
||||
}
|
||||
|
||||
const providerType = this as unknown as Type<AbstractMenuProvider>;
|
||||
return { providerType: providerType, childProviderTypes: childProviders };
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the sections from the provider. These sections can be route dependent.
|
||||
* @param route - The route on which the menu sections possibly depend
|
||||
* @param state - The router snapshot on which the sections possibly depend
|
||||
*/
|
||||
abstract getSections(route?: ActivatedRouteSnapshot, state?: RouterStateSnapshot): Observable<PartialMenuSection[]>;
|
||||
|
||||
protected getAutomatedSectionId(indexOfSectionInProvider: number): string {
|
||||
return `${this.menuProviderId}_${indexOfSectionInProvider}`;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
292
src/app/shared/menu/menu-provider.service.spec.ts
Normal file
292
src/app/shared/menu/menu-provider.service.spec.ts
Normal file
@@ -0,0 +1,292 @@
|
||||
import { waitForAsync } from '@angular/core/testing';
|
||||
import {
|
||||
ActivatedRouteSnapshot,
|
||||
ResolveEnd,
|
||||
RouterStateSnapshot,
|
||||
UrlSegment,
|
||||
} from '@angular/router';
|
||||
import { of as observableOf } from 'rxjs';
|
||||
|
||||
import { COMMUNITY_MODULE_PATH } from '../../community-page/community-page-routing-paths';
|
||||
import { MenuService } from './menu.service';
|
||||
import { MenuID } from './menu-id.model';
|
||||
import { MenuItemType } from './menu-item-type.model';
|
||||
import {
|
||||
AbstractMenuProvider,
|
||||
PartialMenuSection,
|
||||
} from './menu-provider.model';
|
||||
import { MenuProviderService } from './menu-provider.service';
|
||||
import { MenuRoute } from './menu-route.model';
|
||||
|
||||
describe('MenuProviderService', () => {
|
||||
|
||||
class TestMenuProvider extends AbstractMenuProvider {
|
||||
|
||||
constructor(
|
||||
public menuID: MenuID,
|
||||
public shouldPersistOnRouteChange: boolean,
|
||||
public menuProviderId: string,
|
||||
public index: number,
|
||||
public activePaths: MenuRoute[],
|
||||
public parentID: string,
|
||||
public alwaysRenderExpandable: boolean,
|
||||
public sections: PartialMenuSection[],
|
||||
public renderBrowserOnly: boolean = false,
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
getSections(route?: ActivatedRouteSnapshot, state?: RouterStateSnapshot) {
|
||||
return observableOf(this.sections);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
let menuProviderService: MenuProviderService;
|
||||
let menuService: MenuService;
|
||||
|
||||
const router = {
|
||||
events: observableOf(new ResolveEnd(1, 'test-url', 'test-url-after-redirect', {
|
||||
url: 'test-url',
|
||||
root: {
|
||||
url: [new UrlSegment('test-url', {})], data: {},
|
||||
},
|
||||
data: {},
|
||||
} as any)),
|
||||
};
|
||||
|
||||
const section = {
|
||||
visible: true, model: {
|
||||
type: MenuItemType.TEXT,
|
||||
text: `test1`,
|
||||
},
|
||||
};
|
||||
|
||||
const sectionToBeRemoved = {
|
||||
id: 'sectionToBeRemoved',
|
||||
visible: true, model: {
|
||||
type: MenuItemType.TEXT,
|
||||
text: `test1`,
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
const persistentProvider1 = new TestMenuProvider(MenuID.PUBLIC, true, 'provider1', 0, undefined, undefined, false, [section]);
|
||||
const persistentProvider2 = new TestMenuProvider(MenuID.PUBLIC, true, 'provider2', 1, undefined, 'provider1', false, [section, section]);
|
||||
const nonPersistentProvider3 = new TestMenuProvider(MenuID.PUBLIC, false, 'provider3', 2, undefined, undefined, false, [section]);
|
||||
const nonPersistentProvider4 = new TestMenuProvider(MenuID.PUBLIC, false, 'provider4', 3, undefined, 'provider3', false, [section]);
|
||||
const nonPersistentProvider5WithRoutes = new TestMenuProvider(MenuID.PUBLIC, false, 'provider5', 4, [MenuRoute.COMMUNITY_PAGE, MenuRoute.COLLECTION_PAGE], undefined, false, [section]);
|
||||
const nonPersistentProvider6WithRoutesAndBrowserOnlyRendering = new TestMenuProvider(MenuID.PUBLIC, false, 'provider6', 5, [MenuRoute.COMMUNITY_PAGE, MenuRoute.COLLECTION_PAGE], undefined, false, [section], true);
|
||||
const persistentProvider7WithBrowserOnlyRendering = new TestMenuProvider(MenuID.PUBLIC, true, 'provider7', 6, undefined, undefined, false, [section], true);
|
||||
const nonPersistentProvider8WithBrowserOnlyRendering = new TestMenuProvider(MenuID.PUBLIC, false, 'provider8', 3, undefined, undefined, false, [section], true);
|
||||
|
||||
|
||||
|
||||
|
||||
const listOfProvider = [persistentProvider1, persistentProvider2, nonPersistentProvider3, nonPersistentProvider4, nonPersistentProvider5WithRoutes, nonPersistentProvider6WithRoutesAndBrowserOnlyRendering, persistentProvider7WithBrowserOnlyRendering, nonPersistentProvider8WithBrowserOnlyRendering];
|
||||
|
||||
const expectedSection1 = generateAddedSection(persistentProvider1, section);
|
||||
const expectedSection21 = generateAddedSection(persistentProvider2, section);
|
||||
const expectedSection22 = generateAddedSection(persistentProvider2, section, 1);
|
||||
const expectedSection3 = generateAddedSection(nonPersistentProvider3, section);
|
||||
const expectedSection4 = generateAddedSection(nonPersistentProvider4, section);
|
||||
const expectedSection5 = generateAddedSection(nonPersistentProvider5WithRoutes, section);
|
||||
const expectedSection6 = generateAddedSection(nonPersistentProvider6WithRoutesAndBrowserOnlyRendering, section);
|
||||
const expectedSection7 = generateAddedSection(persistentProvider7WithBrowserOnlyRendering, section);
|
||||
const expectedSection8 = generateAddedSection(nonPersistentProvider8WithBrowserOnlyRendering, section);
|
||||
|
||||
function generateAddedSection(provider, sectionToAdd, index = 0) {
|
||||
return {
|
||||
...sectionToAdd,
|
||||
id: sectionToAdd.id ?? `${provider.menuProviderId}_${index}`,
|
||||
parentID: sectionToAdd.parentID ?? provider.parentID,
|
||||
index: sectionToAdd.index ?? provider.index,
|
||||
active: false,
|
||||
shouldPersistOnRouteChange: sectionToAdd.shouldPersistOnRouteChange ?? provider.shouldPersistOnRouteChange,
|
||||
alwaysRenderExpandable: sectionToAdd.alwaysRenderExpandable ?? provider.alwaysRenderExpandable,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
beforeEach(waitForAsync(() => {
|
||||
|
||||
menuService = jasmine.createSpyObj('MenuService',
|
||||
{
|
||||
addSection: {},
|
||||
removeSection: {},
|
||||
getMenu: observableOf({ id: MenuID.PUBLIC }),
|
||||
getNonPersistentMenuSections: observableOf([sectionToBeRemoved]),
|
||||
|
||||
});
|
||||
|
||||
menuProviderService = new MenuProviderService(listOfProvider, menuService, router as any);
|
||||
|
||||
}));
|
||||
|
||||
describe('initPersistentMenus', () => {
|
||||
describe('when server side rendering', () => {
|
||||
it('should initialise the menu sections from the persistent providers while skipping the ones that should not be rendered on SSR', () => {
|
||||
menuProviderService.initPersistentMenus(true);
|
||||
|
||||
expect(menuService.addSection).toHaveBeenCalledWith(persistentProvider1.menuID, expectedSection1);
|
||||
expect(menuService.addSection).toHaveBeenCalledWith(persistentProvider2.menuID, expectedSection21);
|
||||
expect(menuService.addSection).toHaveBeenCalledWith(persistentProvider2.menuID, expectedSection22);
|
||||
expect(menuService.addSection).not.toHaveBeenCalledWith(nonPersistentProvider3.menuID, expectedSection3);
|
||||
expect(menuService.addSection).not.toHaveBeenCalledWith(nonPersistentProvider4.menuID, expectedSection4);
|
||||
expect(menuService.addSection).not.toHaveBeenCalledWith(nonPersistentProvider5WithRoutes.menuID, expectedSection5);
|
||||
expect(menuService.addSection).not.toHaveBeenCalledWith(nonPersistentProvider6WithRoutesAndBrowserOnlyRendering.menuID, expectedSection6);
|
||||
expect(menuService.addSection).not.toHaveBeenCalledWith(persistentProvider7WithBrowserOnlyRendering.menuID, expectedSection7);
|
||||
expect(menuService.addSection).not.toHaveBeenCalledWith(nonPersistentProvider8WithBrowserOnlyRendering.menuID, expectedSection8);
|
||||
});
|
||||
|
||||
});
|
||||
describe('when browser side rendering', () => {
|
||||
it('should initialise the menu sections from the persistent providers without skipping the ones that should not be rendered on SSR', () => {
|
||||
menuProviderService.initPersistentMenus(false);
|
||||
|
||||
expect(menuService.addSection).toHaveBeenCalledWith(persistentProvider1.menuID, expectedSection1);
|
||||
expect(menuService.addSection).toHaveBeenCalledWith(persistentProvider2.menuID, expectedSection21);
|
||||
expect(menuService.addSection).toHaveBeenCalledWith(persistentProvider2.menuID, expectedSection22);
|
||||
expect(menuService.addSection).not.toHaveBeenCalledWith(nonPersistentProvider3.menuID, expectedSection3);
|
||||
expect(menuService.addSection).not.toHaveBeenCalledWith(nonPersistentProvider4.menuID, expectedSection4);
|
||||
expect(menuService.addSection).not.toHaveBeenCalledWith(nonPersistentProvider5WithRoutes.menuID, expectedSection5);
|
||||
expect(menuService.addSection).not.toHaveBeenCalledWith(nonPersistentProvider6WithRoutesAndBrowserOnlyRendering.menuID, expectedSection6);
|
||||
expect(menuService.addSection).toHaveBeenCalledWith(persistentProvider7WithBrowserOnlyRendering.menuID, expectedSection7);
|
||||
expect(menuService.addSection).not.toHaveBeenCalledWith(nonPersistentProvider8WithBrowserOnlyRendering.menuID, expectedSection8);
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
describe('resolveRouteMenus with no matching path specific providers', () => {
|
||||
describe('when browser side rendering', () => {
|
||||
it('should remove the current non persistent menus and add the general non persistent menus', () => {
|
||||
const route = { data: {} };
|
||||
const state = { url: 'test-url' };
|
||||
menuProviderService.resolveRouteMenus(route as any, state as any, false).subscribe();
|
||||
|
||||
expect(menuService.removeSection).toHaveBeenCalledWith(MenuID.PUBLIC, sectionToBeRemoved.id);
|
||||
expect(menuService.removeSection).toHaveBeenCalledWith(MenuID.ADMIN, sectionToBeRemoved.id);
|
||||
expect(menuService.removeSection).toHaveBeenCalledWith(MenuID.DSO_EDIT, sectionToBeRemoved.id);
|
||||
|
||||
expect(menuService.addSection).not.toHaveBeenCalledWith(persistentProvider1.menuID, expectedSection1);
|
||||
expect(menuService.addSection).not.toHaveBeenCalledWith(persistentProvider2.menuID, expectedSection21);
|
||||
expect(menuService.addSection).not.toHaveBeenCalledWith(persistentProvider2.menuID, expectedSection22);
|
||||
expect(menuService.addSection).toHaveBeenCalledWith(nonPersistentProvider3.menuID, expectedSection3);
|
||||
expect(menuService.addSection).toHaveBeenCalledWith(nonPersistentProvider4.menuID, expectedSection4);
|
||||
expect(menuService.addSection).not.toHaveBeenCalledWith(nonPersistentProvider5WithRoutes.menuID, expectedSection5);
|
||||
expect(menuService.addSection).not.toHaveBeenCalledWith(nonPersistentProvider6WithRoutesAndBrowserOnlyRendering.menuID, expectedSection6);
|
||||
expect(menuService.addSection).not.toHaveBeenCalledWith(persistentProvider7WithBrowserOnlyRendering.menuID, expectedSection7);
|
||||
expect(menuService.addSection).toHaveBeenCalledWith(nonPersistentProvider8WithBrowserOnlyRendering.menuID, expectedSection8);
|
||||
});
|
||||
});
|
||||
describe('when server side rendering', () => {
|
||||
it('should remove the current non persistent menus and add the general non persistent menus', () => {
|
||||
const route = { data: {} };
|
||||
const state = { url: 'test-url' };
|
||||
menuProviderService.resolveRouteMenus(route as any, state as any, true).subscribe();
|
||||
|
||||
expect(menuService.removeSection).toHaveBeenCalledWith(MenuID.PUBLIC, sectionToBeRemoved.id);
|
||||
expect(menuService.removeSection).toHaveBeenCalledWith(MenuID.ADMIN, sectionToBeRemoved.id);
|
||||
expect(menuService.removeSection).toHaveBeenCalledWith(MenuID.DSO_EDIT, sectionToBeRemoved.id);
|
||||
|
||||
expect(menuService.addSection).not.toHaveBeenCalledWith(persistentProvider1.menuID, expectedSection1);
|
||||
expect(menuService.addSection).not.toHaveBeenCalledWith(persistentProvider2.menuID, expectedSection21);
|
||||
expect(menuService.addSection).not.toHaveBeenCalledWith(persistentProvider2.menuID, expectedSection22);
|
||||
expect(menuService.addSection).toHaveBeenCalledWith(nonPersistentProvider3.menuID, expectedSection3);
|
||||
expect(menuService.addSection).toHaveBeenCalledWith(nonPersistentProvider4.menuID, expectedSection4);
|
||||
expect(menuService.addSection).not.toHaveBeenCalledWith(nonPersistentProvider5WithRoutes.menuID, expectedSection5);
|
||||
expect(menuService.addSection).not.toHaveBeenCalledWith(nonPersistentProvider6WithRoutesAndBrowserOnlyRendering.menuID, expectedSection6);
|
||||
expect(menuService.addSection).not.toHaveBeenCalledWith(persistentProvider7WithBrowserOnlyRendering.menuID, expectedSection7);
|
||||
expect(menuService.addSection).not.toHaveBeenCalledWith(nonPersistentProvider8WithBrowserOnlyRendering.menuID, expectedSection8);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('resolveRouteMenus with a matching path specific provider', () => {
|
||||
describe('when browser side rendering', () => {
|
||||
it('should remove the current non persistent menus and add the general non persistent menus', () => {
|
||||
const route = { data: { menuRoute: MenuRoute.COMMUNITY_PAGE } };
|
||||
const state = { url: `xxxx/${COMMUNITY_MODULE_PATH}/xxxxxx` };
|
||||
menuProviderService.resolveRouteMenus(route as any, state as any, false).subscribe();
|
||||
|
||||
expect(menuService.removeSection).toHaveBeenCalledWith(MenuID.PUBLIC, sectionToBeRemoved.id);
|
||||
expect(menuService.removeSection).toHaveBeenCalledWith(MenuID.ADMIN, sectionToBeRemoved.id);
|
||||
expect(menuService.removeSection).toHaveBeenCalledWith(MenuID.DSO_EDIT, sectionToBeRemoved.id);
|
||||
|
||||
expect(menuService.addSection).not.toHaveBeenCalledWith(persistentProvider1.menuID, expectedSection1);
|
||||
expect(menuService.addSection).not.toHaveBeenCalledWith(persistentProvider2.menuID, expectedSection21);
|
||||
expect(menuService.addSection).not.toHaveBeenCalledWith(persistentProvider2.menuID, expectedSection22);
|
||||
expect(menuService.addSection).toHaveBeenCalledWith(nonPersistentProvider3.menuID, expectedSection3);
|
||||
expect(menuService.addSection).toHaveBeenCalledWith(nonPersistentProvider4.menuID, expectedSection4);
|
||||
expect(menuService.addSection).toHaveBeenCalledWith(nonPersistentProvider5WithRoutes.menuID, expectedSection5);
|
||||
expect(menuService.addSection).toHaveBeenCalledWith(nonPersistentProvider6WithRoutesAndBrowserOnlyRendering.menuID, expectedSection6);
|
||||
expect(menuService.addSection).not.toHaveBeenCalledWith(persistentProvider7WithBrowserOnlyRendering.menuID, expectedSection7);
|
||||
expect(menuService.addSection).toHaveBeenCalledWith(nonPersistentProvider8WithBrowserOnlyRendering.menuID, expectedSection8);
|
||||
});
|
||||
});
|
||||
describe('when server side rendering', () => {
|
||||
it('should remove the current non persistent menus and add the general non persistent menus', () => {
|
||||
const route = { data: { menuRoute: MenuRoute.COMMUNITY_PAGE } };
|
||||
const state = { url: `xxxx/${COMMUNITY_MODULE_PATH}/xxxxxx` };
|
||||
menuProviderService.resolveRouteMenus(route as any, state as any, true).subscribe();
|
||||
|
||||
expect(menuService.removeSection).toHaveBeenCalledWith(MenuID.PUBLIC, sectionToBeRemoved.id);
|
||||
expect(menuService.removeSection).toHaveBeenCalledWith(MenuID.ADMIN, sectionToBeRemoved.id);
|
||||
expect(menuService.removeSection).toHaveBeenCalledWith(MenuID.DSO_EDIT, sectionToBeRemoved.id);
|
||||
|
||||
expect(menuService.addSection).not.toHaveBeenCalledWith(persistentProvider1.menuID, expectedSection1);
|
||||
expect(menuService.addSection).not.toHaveBeenCalledWith(persistentProvider2.menuID, expectedSection21);
|
||||
expect(menuService.addSection).not.toHaveBeenCalledWith(persistentProvider2.menuID, expectedSection22);
|
||||
expect(menuService.addSection).toHaveBeenCalledWith(nonPersistentProvider3.menuID, expectedSection3);
|
||||
expect(menuService.addSection).toHaveBeenCalledWith(nonPersistentProvider4.menuID, expectedSection4);
|
||||
expect(menuService.addSection).toHaveBeenCalledWith(nonPersistentProvider5WithRoutes.menuID, expectedSection5);
|
||||
expect(menuService.addSection).not.toHaveBeenCalledWith(nonPersistentProvider6WithRoutesAndBrowserOnlyRendering.menuID, expectedSection6);
|
||||
expect(menuService.addSection).not.toHaveBeenCalledWith(persistentProvider7WithBrowserOnlyRendering.menuID, expectedSection7);
|
||||
expect(menuService.addSection).not.toHaveBeenCalledWith(nonPersistentProvider8WithBrowserOnlyRendering.menuID, expectedSection8);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('listenForRouteChanges ', () => {
|
||||
describe('when browser side rendering', () => {
|
||||
it('should listen to the route changes and update the menu sections based on the retrieved state and route', () => {
|
||||
menuProviderService.listenForRouteChanges(false);
|
||||
|
||||
expect(menuService.removeSection).toHaveBeenCalledWith(MenuID.PUBLIC, sectionToBeRemoved.id);
|
||||
expect(menuService.removeSection).toHaveBeenCalledWith(MenuID.ADMIN, sectionToBeRemoved.id);
|
||||
expect(menuService.removeSection).toHaveBeenCalledWith(MenuID.DSO_EDIT, sectionToBeRemoved.id);
|
||||
|
||||
expect(menuService.addSection).not.toHaveBeenCalledWith(persistentProvider1.menuID, expectedSection1);
|
||||
expect(menuService.addSection).not.toHaveBeenCalledWith(persistentProvider2.menuID, expectedSection21);
|
||||
expect(menuService.addSection).not.toHaveBeenCalledWith(persistentProvider2.menuID, expectedSection22);
|
||||
expect(menuService.addSection).toHaveBeenCalledWith(nonPersistentProvider3.menuID, expectedSection3);
|
||||
expect(menuService.addSection).toHaveBeenCalledWith(nonPersistentProvider4.menuID, expectedSection4);
|
||||
expect(menuService.addSection).not.toHaveBeenCalledWith(nonPersistentProvider5WithRoutes.menuID, expectedSection5);
|
||||
expect(menuService.addSection).not.toHaveBeenCalledWith(nonPersistentProvider6WithRoutesAndBrowserOnlyRendering.menuID, expectedSection6);
|
||||
expect(menuService.addSection).not.toHaveBeenCalledWith(persistentProvider7WithBrowserOnlyRendering.menuID, expectedSection7);
|
||||
expect(menuService.addSection).toHaveBeenCalledWith(nonPersistentProvider8WithBrowserOnlyRendering.menuID, expectedSection8);
|
||||
});
|
||||
});
|
||||
describe('when server side rendering', () => {
|
||||
it('should listen to the route changes and update the menu sections based on the retrieved state and route', () => {
|
||||
menuProviderService.listenForRouteChanges(true);
|
||||
|
||||
expect(menuService.removeSection).toHaveBeenCalledWith(MenuID.PUBLIC, sectionToBeRemoved.id);
|
||||
expect(menuService.removeSection).toHaveBeenCalledWith(MenuID.ADMIN, sectionToBeRemoved.id);
|
||||
expect(menuService.removeSection).toHaveBeenCalledWith(MenuID.DSO_EDIT, sectionToBeRemoved.id);
|
||||
|
||||
expect(menuService.addSection).not.toHaveBeenCalledWith(persistentProvider1.menuID, expectedSection1);
|
||||
expect(menuService.addSection).not.toHaveBeenCalledWith(persistentProvider2.menuID, expectedSection21);
|
||||
expect(menuService.addSection).not.toHaveBeenCalledWith(persistentProvider2.menuID, expectedSection22);
|
||||
expect(menuService.addSection).toHaveBeenCalledWith(nonPersistentProvider3.menuID, expectedSection3);
|
||||
expect(menuService.addSection).toHaveBeenCalledWith(nonPersistentProvider4.menuID, expectedSection4);
|
||||
expect(menuService.addSection).not.toHaveBeenCalledWith(nonPersistentProvider5WithRoutes.menuID, expectedSection5);
|
||||
expect(menuService.addSection).not.toHaveBeenCalledWith(nonPersistentProvider6WithRoutesAndBrowserOnlyRendering.menuID, expectedSection6);
|
||||
expect(menuService.addSection).not.toHaveBeenCalledWith(persistentProvider7WithBrowserOnlyRendering.menuID, expectedSection7);
|
||||
expect(menuService.addSection).not.toHaveBeenCalledWith(nonPersistentProvider8WithBrowserOnlyRendering.menuID, expectedSection8);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
227
src/app/shared/menu/menu-provider.service.ts
Normal file
227
src/app/shared/menu/menu-provider.service.ts
Normal file
@@ -0,0 +1,227 @@
|
||||
/**
|
||||
* The contents of this file are subject to the license and copyright
|
||||
* detailed in the LICENSE and NOTICE files at the root of the source
|
||||
* tree and available online at
|
||||
*
|
||||
* http://www.dspace.org/license/
|
||||
*/
|
||||
|
||||
import {
|
||||
Inject,
|
||||
Injectable,
|
||||
Optional,
|
||||
} from '@angular/core';
|
||||
import {
|
||||
ActivatedRouteSnapshot,
|
||||
ResolveEnd,
|
||||
Router,
|
||||
RouterStateSnapshot,
|
||||
} from '@angular/router';
|
||||
import {
|
||||
combineLatest,
|
||||
map,
|
||||
Observable,
|
||||
} from 'rxjs';
|
||||
import {
|
||||
filter,
|
||||
find,
|
||||
switchMap,
|
||||
take,
|
||||
} from 'rxjs/operators';
|
||||
|
||||
import {
|
||||
hasValue,
|
||||
isNotEmpty,
|
||||
} from '../empty.util';
|
||||
import { MenuService } from './menu.service';
|
||||
import { MENU_PROVIDER } from './menu.structure';
|
||||
import { MenuID } from './menu-id.model';
|
||||
import {
|
||||
AbstractMenuProvider,
|
||||
PartialMenuSection,
|
||||
} from './menu-provider.model';
|
||||
import { MenuRoute } from './menu-route.model';
|
||||
import { MenuState } from './menu-state.model';
|
||||
|
||||
/**
|
||||
* Service that is responsible for adding and removing the menu sections created by the providers, both for
|
||||
* persistent and non-persistent menu sections
|
||||
*/
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class MenuProviderService {
|
||||
constructor(
|
||||
@Inject(MENU_PROVIDER) @Optional() protected providers: ReadonlyArray<AbstractMenuProvider>,
|
||||
protected menuService: MenuService,
|
||||
protected router: Router,
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Wait for a specific menu to appear
|
||||
* @param id the ID of the menu to wait for
|
||||
* @return an Observable that emits true as soon as the menu is created
|
||||
*/
|
||||
protected waitForMenu$(id: MenuID): Observable<boolean> {
|
||||
return this.menuService.getMenu(id).pipe(
|
||||
find((menu: MenuState) => hasValue(menu)),
|
||||
map(() => true),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Listen for route changes and resolve the route dependent menu sections on route change
|
||||
*/
|
||||
listenForRouteChanges(isServerRendering) {
|
||||
this.router.events.pipe(
|
||||
filter(event => event instanceof ResolveEnd),
|
||||
switchMap((event: ResolveEnd) => {
|
||||
const currentRoute = this.getCurrentRoute(event.state.root);
|
||||
return this.resolveRouteMenus(currentRoute, event.state, isServerRendering);
|
||||
}),
|
||||
).subscribe();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the full current route
|
||||
*/
|
||||
private getCurrentRoute(route: ActivatedRouteSnapshot): ActivatedRouteSnapshot {
|
||||
while (route.firstChild) {
|
||||
route = route.firstChild;
|
||||
}
|
||||
return route;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Initialise the persistent menu sections
|
||||
*/
|
||||
public initPersistentMenus(isServerRendering) {
|
||||
combineLatest([
|
||||
...this.providers
|
||||
.map((provider) => {
|
||||
return provider;
|
||||
})
|
||||
.filter(provider => !(isServerRendering && provider.renderBrowserOnly))
|
||||
.filter(provider => provider.shouldPersistOnRouteChange)
|
||||
.map(provider => provider.getSections()
|
||||
.pipe(
|
||||
map((sections) => {
|
||||
return { provider: provider, sections: sections };
|
||||
}),
|
||||
),
|
||||
)])
|
||||
.pipe(
|
||||
switchMap((providerWithSections: { provider: AbstractMenuProvider, sections: PartialMenuSection[] }[]) => {
|
||||
const waitForMenus = providerWithSections.map((providerWithSection: {
|
||||
provider: AbstractMenuProvider,
|
||||
sections: PartialMenuSection[]
|
||||
}, sectionIndex) => {
|
||||
providerWithSection.sections.forEach((section, index) => {
|
||||
this.addSection(providerWithSection.provider, section, index);
|
||||
});
|
||||
return this.waitForMenu$(providerWithSection.provider.menuID);
|
||||
});
|
||||
return [waitForMenus];
|
||||
}),
|
||||
map(done => done.every(Boolean)),
|
||||
take(1),
|
||||
).subscribe();
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve the non-persistent route based menu sections
|
||||
* @param route - the current route
|
||||
* @param state - the current router state
|
||||
* @param isServerRendering - whether server side rendering is true
|
||||
*/
|
||||
public resolveRouteMenus(
|
||||
route: ActivatedRouteSnapshot,
|
||||
state: RouterStateSnapshot,
|
||||
isServerRendering,
|
||||
): Observable<boolean> {
|
||||
const currentNonPersistentMenuSections$ = combineLatest([
|
||||
...Object.values(MenuID).map((menuID) => {
|
||||
return this.menuService.getNonPersistentMenuSections(menuID).pipe(
|
||||
take(1),
|
||||
map((sections) => {
|
||||
return { menuId: menuID, sections: sections };
|
||||
}));
|
||||
})]);
|
||||
|
||||
const routeDependentMenuSections$ = combineLatest([
|
||||
...this.providers
|
||||
.filter(provider => !(isServerRendering && provider.renderBrowserOnly))
|
||||
.filter(provider => {
|
||||
let shouldUpdate = false;
|
||||
if (!provider.shouldPersistOnRouteChange && isNotEmpty(provider.activePaths)) {
|
||||
provider.activePaths.forEach((path: MenuRoute) => {
|
||||
if (route.data.menuRoute === path) {
|
||||
shouldUpdate = true;
|
||||
}
|
||||
});
|
||||
} else if (!provider.shouldPersistOnRouteChange) {
|
||||
shouldUpdate = true;
|
||||
}
|
||||
return shouldUpdate;
|
||||
})
|
||||
.map(provider => provider.getSections(route, state)
|
||||
.pipe(
|
||||
map((sections) => {
|
||||
return { provider: provider, sections: sections };
|
||||
}),
|
||||
),
|
||||
),
|
||||
]);
|
||||
|
||||
return combineLatest([
|
||||
currentNonPersistentMenuSections$,
|
||||
routeDependentMenuSections$,
|
||||
]).pipe(
|
||||
switchMap(([currentMenusWithSections, providerWithSections]) => {
|
||||
this.removeNonPersistentSections(currentMenusWithSections);
|
||||
const waitForMenus = providerWithSections.map((providerWithSection: {
|
||||
provider: AbstractMenuProvider,
|
||||
sections: PartialMenuSection[]
|
||||
}) => {
|
||||
providerWithSection.sections.forEach((section, index) => {
|
||||
this.addSection(providerWithSection.provider, section, index);
|
||||
});
|
||||
return this.waitForMenu$(providerWithSection.provider.menuID);
|
||||
});
|
||||
return [waitForMenus];
|
||||
}),
|
||||
map(done => done.every(Boolean)),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the provided section combined with information from the menu provider to the menus
|
||||
* @param provider - The provider of the section which will be used to provide extra data to the section
|
||||
* @param section - The partial section to be added to the menus
|
||||
*/
|
||||
private addSection(provider: AbstractMenuProvider, section: PartialMenuSection, index: number) {
|
||||
this.menuService.addSection(provider.menuID, {
|
||||
...section,
|
||||
id: section.id ?? `${provider.menuProviderId}_${index}`,
|
||||
parentID: section.parentID ?? provider.parentID,
|
||||
index: provider.index,
|
||||
active: section.active ?? false,
|
||||
shouldPersistOnRouteChange: section.shouldPersistOnRouteChange ?? provider.shouldPersistOnRouteChange,
|
||||
alwaysRenderExpandable: section.alwaysRenderExpandable ?? provider.alwaysRenderExpandable,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove all non-persistent sections from the menus
|
||||
* @param menuWithSections - The menu with its sections to be removed
|
||||
*/
|
||||
private removeNonPersistentSections(menuWithSections) {
|
||||
menuWithSections.forEach((menu) => {
|
||||
menu.sections.forEach((section) => {
|
||||
this.menuService.removeSection(menu.menuId, section.id);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
8
src/app/shared/menu/menu-route.model.ts
Normal file
8
src/app/shared/menu/menu-route.model.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
/**
|
||||
* The menu route IDs that can be used for route resolvers
|
||||
*/
|
||||
export enum MenuRoute {
|
||||
COMMUNITY_PAGE = 'community-page',
|
||||
COLLECTION_PAGE = 'collection-page',
|
||||
ITEM_PAGE = 'item-page',
|
||||
}
|
@@ -1,15 +1,72 @@
|
||||
import { MenuItemModel } from './menu-item/models/menu-item.model';
|
||||
import { AltmetricMenuItemModel } from './menu-item/models/altmetric.model';
|
||||
import { ExternalLinkMenuItemModel } from './menu-item/models/external-link.model';
|
||||
import { LinkMenuItemModel } from './menu-item/models/link.model';
|
||||
import { OnClickMenuItemModel } from './menu-item/models/onclick.model';
|
||||
import { SearchMenuItemModel } from './menu-item/models/search.model';
|
||||
import { TextMenuItemModel } from './menu-item/models/text.model';
|
||||
|
||||
export type MenuItemModels =
|
||||
LinkMenuItemModel
|
||||
| AltmetricMenuItemModel
|
||||
| ExternalLinkMenuItemModel
|
||||
| OnClickMenuItemModel
|
||||
| SearchMenuItemModel
|
||||
| TextMenuItemModel;
|
||||
|
||||
export interface MenuSection {
|
||||
/**
|
||||
* The identifier for this section
|
||||
*/
|
||||
id: string;
|
||||
|
||||
/**
|
||||
* Represents the state of a single menu section in the store
|
||||
* Accessibility handle that can be used to find a specific menu in the html
|
||||
*/
|
||||
accessibilityHandle?: string;
|
||||
|
||||
/**
|
||||
* Whether this section should be visible.
|
||||
*/
|
||||
export class MenuSection {
|
||||
id: string;
|
||||
parentID?: string;
|
||||
visible: boolean;
|
||||
active: boolean;
|
||||
model: MenuItemModel;
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
model: MenuItemModels;
|
||||
|
||||
/**
|
||||
* The identifier of this section's parent section (optional).
|
||||
*/
|
||||
parentID?: string;
|
||||
|
||||
/**
|
||||
* The index of this section in its menu.
|
||||
*/
|
||||
index?: number;
|
||||
|
||||
/**
|
||||
* Whether this section is currently active.
|
||||
* Newly created sections are inactive until toggled.
|
||||
*/
|
||||
active?: boolean;
|
||||
|
||||
/**
|
||||
* Whether this section is independent of the route (default: true).
|
||||
* This value should be set explicitly for route-dependent sections.
|
||||
*/
|
||||
shouldPersistOnRouteChange?: boolean;
|
||||
|
||||
|
||||
/**
|
||||
* An optional icon for this section.
|
||||
* Must correspond to a FontAwesome icon class save for the `.fa-` prefix.
|
||||
* Note that not all menus may render icons.
|
||||
*/
|
||||
icon?: string;
|
||||
shouldPersistOnRouteChange? = false;
|
||||
|
||||
/**
|
||||
* When true, the current section will be assumed to be a parent section with children
|
||||
* This section will not be rendered when it has no visible children
|
||||
*/
|
||||
alwaysRenderExpandable?: boolean;
|
||||
}
|
||||
|
@@ -34,7 +34,8 @@ import { MenuSection } from '../menu-section.model';
|
||||
template: '',
|
||||
standalone: true,
|
||||
})
|
||||
export class MenuSectionComponent implements OnInit, OnDestroy {
|
||||
export abstract class AbstractMenuSectionComponent implements OnInit, OnDestroy {
|
||||
protected abstract section: MenuSection;
|
||||
|
||||
/**
|
||||
* {@link BehaviorSubject} containing the current state to whether this section is currently active
|
||||
@@ -56,7 +57,7 @@ export class MenuSectionComponent implements OnInit, OnDestroy {
|
||||
*/
|
||||
sectionMap$: BehaviorSubject<Map<string, {
|
||||
injector: Injector,
|
||||
component: GenericConstructor<MenuSectionComponent>
|
||||
component: GenericConstructor<AbstractMenuSectionComponent>
|
||||
}>> = new BehaviorSubject(new Map());
|
||||
|
||||
/**
|
||||
@@ -65,7 +66,10 @@ export class MenuSectionComponent implements OnInit, OnDestroy {
|
||||
*/
|
||||
subs: Subscription[] = [];
|
||||
|
||||
constructor(public section: MenuSection, protected menuService: MenuService, protected injector: Injector) {
|
||||
protected constructor(
|
||||
protected menuService: MenuService,
|
||||
protected injector: Injector,
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
@@ -1,5 +1,7 @@
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
Inject,
|
||||
Injector,
|
||||
NO_ERRORS_SCHEMA,
|
||||
} from '@angular/core';
|
||||
@@ -16,11 +18,26 @@ import { MenuServiceStub } from '../../testing/menu-service.stub';
|
||||
import { MenuService } from '../menu.service';
|
||||
import { LinkMenuItemComponent } from '../menu-item/link-menu-item.component';
|
||||
import { MenuSection } from '../menu-section.model';
|
||||
import { MenuSectionComponent } from './menu-section.component';
|
||||
import { AbstractMenuSectionComponent } from './abstract-menu-section.component';
|
||||
|
||||
@Component({
|
||||
selector: 'ds-some-menu-section',
|
||||
template: '',
|
||||
standalone: true,
|
||||
})
|
||||
class SomeMenuSectionComponent extends AbstractMenuSectionComponent {
|
||||
constructor(
|
||||
@Inject('sectionDataProvider') protected section: MenuSection,
|
||||
protected menuService: MenuService,
|
||||
protected injector: Injector,
|
||||
) {
|
||||
super(menuService, injector);
|
||||
}
|
||||
}
|
||||
|
||||
describe('MenuSectionComponent', () => {
|
||||
let comp: MenuSectionComponent;
|
||||
let fixture: ComponentFixture<MenuSectionComponent>;
|
||||
let comp: AbstractMenuSectionComponent;
|
||||
let fixture: ComponentFixture<AbstractMenuSectionComponent>;
|
||||
let menuService: MenuService;
|
||||
let dummySection;
|
||||
|
||||
@@ -31,20 +48,20 @@ describe('MenuSectionComponent', () => {
|
||||
active: false,
|
||||
} as any;
|
||||
TestBed.configureTestingModule({
|
||||
imports: [TranslateModule.forRoot(), NoopAnimationsModule, MenuSectionComponent],
|
||||
imports: [TranslateModule.forRoot(), NoopAnimationsModule, SomeMenuSectionComponent, AbstractMenuSectionComponent],
|
||||
providers: [
|
||||
{ provide: Injector, useValue: {} },
|
||||
{ provide: MenuService, useClass: MenuServiceStub },
|
||||
{ provide: MenuSection, useValue: dummySection },
|
||||
{ provide: 'sectionDataProvider', useValue: dummySection },
|
||||
],
|
||||
schemas: [NO_ERRORS_SCHEMA],
|
||||
}).overrideComponent(MenuSectionComponent, {
|
||||
}).overrideComponent(SomeMenuSectionComponent, {
|
||||
set: { changeDetection: ChangeDetectionStrategy.Default },
|
||||
}).compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(MenuSectionComponent);
|
||||
fixture = TestBed.createComponent(SomeMenuSectionComponent);
|
||||
comp = fixture.componentInstance;
|
||||
menuService = (comp as any).menuService;
|
||||
spyOn(comp as any, 'getMenuItemComponent').and.returnValue(LinkMenuItemComponent);
|
||||
|
@@ -1,5 +1,5 @@
|
||||
import { MenuID } from './menu-id.model';
|
||||
import { MenuSectionIndex } from './menu-section-Index.model';
|
||||
import { MenuSectionIndex } from './menu-section-index.model';
|
||||
import { MenuSections } from './menu-sections.model';
|
||||
|
||||
/**
|
||||
|
@@ -43,6 +43,7 @@ import { MenuComponent } from './menu.component';
|
||||
import { MenuService } from './menu.service';
|
||||
import { MenuID } from './menu-id.model';
|
||||
import { LinkMenuItemModel } from './menu-item/models/link.model';
|
||||
import { TextMenuItemModel } from './menu-item/models/text.model';
|
||||
import { MenuItemType } from './menu-item-type.model';
|
||||
import { rendersSectionForMenu } from './menu-section.decorator';
|
||||
import { MenuSection } from './menu-section.model';
|
||||
@@ -53,6 +54,7 @@ const mockMenuID = 'mock-menuID' as MenuID;
|
||||
// eslint-disable-next-line @angular-eslint/component-selector
|
||||
selector: '',
|
||||
template: '',
|
||||
standalone: true,
|
||||
})
|
||||
@rendersSectionForMenu(mockMenuID, true)
|
||||
class TestExpandableMenuComponent {
|
||||
@@ -62,6 +64,7 @@ class TestExpandableMenuComponent {
|
||||
// eslint-disable-next-line @angular-eslint/component-selector
|
||||
selector: '',
|
||||
template: '',
|
||||
standalone: true,
|
||||
})
|
||||
@rendersSectionForMenu(mockMenuID, false)
|
||||
class TestMenuComponent {
|
||||
@@ -72,6 +75,17 @@ describe('MenuComponent', () => {
|
||||
let fixture: ComponentFixture<MenuComponent>;
|
||||
let menuService: MenuService;
|
||||
let store: MockStore;
|
||||
let router: any;
|
||||
|
||||
const menuSection: MenuSection = {
|
||||
id: 'browse',
|
||||
model: {
|
||||
type: MenuItemType.TEXT,
|
||||
text: 'menu.section.browse_global',
|
||||
} as TextMenuItemModel,
|
||||
icon: 'globe',
|
||||
visible: true,
|
||||
};
|
||||
|
||||
const mockStatisticSection = { 'id': 'statistics_site', 'active': true, 'visible': true, 'index': 2, 'type': 'statistics', 'model': { 'type': 1, 'text': 'menu.section.statistics', 'link': 'statistics' } };
|
||||
|
||||
@@ -111,6 +125,7 @@ describe('MenuComponent', () => {
|
||||
id: 'section1',
|
||||
active: false,
|
||||
visible: true,
|
||||
alwaysRenderExpandable: false,
|
||||
model: {
|
||||
type: MenuItemType.LINK,
|
||||
text: 'test',
|
||||
@@ -130,7 +145,7 @@ describe('MenuComponent', () => {
|
||||
});
|
||||
|
||||
TestBed.configureTestingModule({
|
||||
imports: [TranslateModule.forRoot(), NoopAnimationsModule, RouterTestingModule, MenuComponent, StoreModule.forRoot(authReducer, storeModuleConfig)],
|
||||
imports: [TranslateModule.forRoot(), NoopAnimationsModule, RouterTestingModule, MenuComponent, StoreModule.forRoot(authReducer, storeModuleConfig), TestExpandableMenuComponent, TestMenuComponent],
|
||||
providers: [
|
||||
Injector,
|
||||
{ provide: ThemeService, useValue: getMockThemeService() },
|
||||
@@ -138,8 +153,6 @@ describe('MenuComponent', () => {
|
||||
provideMockStore({ initialState }),
|
||||
{ provide: AuthorizationDataService, useValue: authorizationService },
|
||||
{ provide: ActivatedRoute, useValue: routeStub },
|
||||
TestExpandableMenuComponent,
|
||||
TestMenuComponent,
|
||||
],
|
||||
schemas: [NO_ERRORS_SCHEMA],
|
||||
}).overrideComponent(MenuComponent, {
|
||||
@@ -153,7 +166,7 @@ describe('MenuComponent', () => {
|
||||
comp.menuID = mockMenuID;
|
||||
menuService = TestBed.inject(MenuService);
|
||||
store = TestBed.inject(Store) as MockStore<AppState>;
|
||||
spyOn(comp as any, 'getSectionDataInjector').and.returnValue(MenuSection);
|
||||
spyOn(comp as any, 'getSectionDataInjector').and.returnValue(menuSection);
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
@@ -178,6 +191,7 @@ describe('MenuComponent', () => {
|
||||
id: 'section1',
|
||||
active: false,
|
||||
visible: true,
|
||||
alwaysRenderExpandable: false,
|
||||
model: {
|
||||
type: MenuItemType.LINK,
|
||||
text: 'test',
|
||||
@@ -189,6 +203,7 @@ describe('MenuComponent', () => {
|
||||
parentID: 'section1',
|
||||
active: false,
|
||||
visible: true,
|
||||
alwaysRenderExpandable: false,
|
||||
model: {
|
||||
type: MenuItemType.LINK,
|
||||
text: 'test',
|
||||
@@ -258,35 +273,4 @@ describe('MenuComponent', () => {
|
||||
expect(menuService.collapseMenuPreview).toHaveBeenCalledWith(comp.menuID);
|
||||
}));
|
||||
});
|
||||
|
||||
describe('when unauthorized statistics', () => {
|
||||
|
||||
beforeEach(() => {
|
||||
(authorizationService as any).isAuthorized.and.returnValue(observableOf(false));
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should return observable of empty object', done => {
|
||||
comp.getAuthorizedStatistics(mockStatisticSection).subscribe((res) => {
|
||||
expect(res).toEqual({});
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('get authorized statistics', () => {
|
||||
|
||||
beforeEach(() => {
|
||||
(authorizationService as any).isAuthorized.and.returnValue(observableOf(true));
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should return observable of statistics section menu', done => {
|
||||
comp.getAuthorizedStatistics(mockStatisticSection).subscribe((res) => {
|
||||
expect(res).toEqual(mockStatisticSection);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
@@ -9,18 +9,15 @@ import { ActivatedRoute } from '@angular/router';
|
||||
import {
|
||||
BehaviorSubject,
|
||||
Observable,
|
||||
of as observableOf,
|
||||
Subscription,
|
||||
} from 'rxjs';
|
||||
import {
|
||||
distinctUntilChanged,
|
||||
map,
|
||||
mergeMap,
|
||||
switchMap,
|
||||
} from 'rxjs/operators';
|
||||
|
||||
import { AuthorizationDataService } from '../../core/data/feature-authorization/authorization-data.service';
|
||||
import { FeatureID } from '../../core/data/feature-authorization/feature-id';
|
||||
import { GenericConstructor } from '../../core/shared/generic-constructor';
|
||||
import {
|
||||
hasValue,
|
||||
@@ -31,7 +28,7 @@ import { MenuService } from './menu.service';
|
||||
import { MenuID } from './menu-id.model';
|
||||
import { getComponentForMenu } from './menu-section.decorator';
|
||||
import { MenuSection } from './menu-section.model';
|
||||
import { MenuSectionComponent } from './menu-section/menu-section.component';
|
||||
import { AbstractMenuSectionComponent } from './menu-section/abstract-menu-section.component';
|
||||
|
||||
/**
|
||||
* A basic implementation of a MenuComponent
|
||||
@@ -72,7 +69,7 @@ export class MenuComponent implements OnInit, OnDestroy {
|
||||
*/
|
||||
sectionMap$: BehaviorSubject<Map<string, {
|
||||
injector: Injector,
|
||||
component: GenericConstructor<MenuSectionComponent>
|
||||
component: GenericConstructor<AbstractMenuSectionComponent>
|
||||
}>> = new BehaviorSubject(new Map());
|
||||
|
||||
/**
|
||||
@@ -93,8 +90,12 @@ export class MenuComponent implements OnInit, OnDestroy {
|
||||
|
||||
private activatedRouteLastChild: ActivatedRoute;
|
||||
|
||||
constructor(protected menuService: MenuService, protected injector: Injector, public authorizationService: AuthorizationDataService,
|
||||
public route: ActivatedRoute, protected themeService: ThemeService,
|
||||
constructor(
|
||||
protected menuService: MenuService,
|
||||
protected injector: Injector,
|
||||
public authorizationService: AuthorizationDataService,
|
||||
public route: ActivatedRoute,
|
||||
protected themeService: ThemeService,
|
||||
) {
|
||||
}
|
||||
|
||||
@@ -113,15 +114,9 @@ export class MenuComponent implements OnInit, OnDestroy {
|
||||
// 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
|
||||
switchMap((sections: MenuSection[]) => sections),
|
||||
mergeMap((section: MenuSection) => {
|
||||
if (section.id.includes('statistics')) {
|
||||
return this.getAuthorizedStatistics(section);
|
||||
}
|
||||
return observableOf(section);
|
||||
}),
|
||||
isNotEmptyOperator(),
|
||||
switchMap((section: MenuSection) => this.getSectionComponent(section).pipe(
|
||||
map((component: GenericConstructor<MenuSectionComponent>) => ({ section, component })),
|
||||
map((component: GenericConstructor<AbstractMenuSectionComponent>) => ({ section, component })),
|
||||
)),
|
||||
distinctUntilChanged((x, y) => x.section.id === y.section.id && x.component.prototype === y.component.prototype),
|
||||
).subscribe(({ section, component }) => {
|
||||
@@ -146,32 +141,6 @@ export class MenuComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
* @param {Event} event The user event that triggered this method
|
||||
@@ -233,12 +202,12 @@ export class MenuComponent implements OnInit, OnDestroy {
|
||||
/**
|
||||
* Retrieve the component for a given MenuSection object
|
||||
* @param {MenuSection} section The given MenuSection
|
||||
* @returns {Observable<GenericConstructor<MenuSectionComponent>>} Emits the constructor of the Component that should be used to render this object
|
||||
* @returns {Observable<GenericConstructor<AbstractMenuSectionComponent>>} Emits the constructor of the Component that should be used to render this object
|
||||
*/
|
||||
private getSectionComponent(section: MenuSection): Observable<GenericConstructor<MenuSectionComponent>> {
|
||||
private getSectionComponent(section: MenuSection): Observable<GenericConstructor<AbstractMenuSectionComponent>> {
|
||||
return this.menuService.hasSubSections(this.menuID, section.id).pipe(
|
||||
map((expandable: boolean) => {
|
||||
return getComponentForMenu(this.menuID, expandable, this.themeService.getThemeName());
|
||||
return getComponentForMenu(this.menuID, expandable || section.alwaysRenderExpandable, this.themeService.getThemeName());
|
||||
},
|
||||
),
|
||||
);
|
||||
|
@@ -21,7 +21,7 @@ import {
|
||||
} from './menu.actions';
|
||||
import { menusReducer } from './menu.reducer';
|
||||
import { MenuID } from './menu-id.model';
|
||||
import { MenuSectionIndex } from './menu-section-Index.model';
|
||||
import { MenuSectionIndex } from './menu-section-index.model';
|
||||
|
||||
let visibleSection1;
|
||||
let dummyState;
|
||||
|
@@ -14,7 +14,7 @@ import {
|
||||
} from './menu.actions';
|
||||
import { MenuID } from './menu-id.model';
|
||||
import { MenuSection } from './menu-section.model';
|
||||
import { MenuSectionIndex } from './menu-section-Index.model';
|
||||
import { MenuSectionIndex } from './menu-section-index.model';
|
||||
import { MenuSections } from './menu-sections.model';
|
||||
import { MenuState } from './menu-state.model';
|
||||
import { MenusState } from './menus-state.model';
|
||||
|
@@ -44,7 +44,6 @@ describe('MenuService', () => {
|
||||
let topSections;
|
||||
let initialState;
|
||||
let routeDataMenuSection: MenuSection;
|
||||
let routeDataMenuSectionResolved: MenuSection;
|
||||
let routeDataMenuChildSection: MenuSection;
|
||||
let routeDataMenuOverwrittenChildSection: MenuSection;
|
||||
let toBeRemovedMenuSection: MenuSection;
|
||||
@@ -112,16 +111,6 @@ describe('MenuService', () => {
|
||||
link: 'path/:linkparam',
|
||||
} as LinkMenuItemModel,
|
||||
};
|
||||
routeDataMenuSectionResolved = {
|
||||
id: 'mockSection_id_param_resolved',
|
||||
active: false,
|
||||
visible: true,
|
||||
model: {
|
||||
type: MenuItemType.LINK,
|
||||
text: 'menu.section.mockSection',
|
||||
link: 'path/link_param_resolved',
|
||||
} as LinkMenuItemModel,
|
||||
};
|
||||
routeDataMenuChildSection = {
|
||||
id: 'mockChildSection',
|
||||
parentID: 'mockSection',
|
||||
@@ -144,16 +133,6 @@ describe('MenuService', () => {
|
||||
link: '',
|
||||
} as LinkMenuItemModel,
|
||||
};
|
||||
toBeRemovedMenuSection = {
|
||||
id: 'toBeRemovedSection',
|
||||
active: false,
|
||||
visible: true,
|
||||
model: {
|
||||
type: MenuItemType.LINK,
|
||||
text: 'menu.section.toBeRemovedSection',
|
||||
link: '',
|
||||
} as LinkMenuItemModel,
|
||||
};
|
||||
alreadyPresentMenuSection = {
|
||||
id: 'alreadyPresentSection',
|
||||
active: false,
|
||||
@@ -566,70 +545,4 @@ describe('MenuService', () => {
|
||||
expect(store.dispatch).toHaveBeenCalledWith(new DeactivateMenuSectionAction(MenuID.ADMIN, 'fakeID'));
|
||||
});
|
||||
});
|
||||
|
||||
describe('buildRouteMenuSections', () => {
|
||||
it('should add and remove menu sections depending on the current route and overwrite menu sections when they have the same ID with the child route version', () => {
|
||||
spyOn(service, 'addSection');
|
||||
spyOn(service, 'removeSection');
|
||||
|
||||
spyOn(service, 'getNonPersistentMenuSections').and.returnValue(observableOf([toBeRemovedMenuSection, alreadyPresentMenuSection]));
|
||||
|
||||
service.buildRouteMenuSections(MenuID.PUBLIC);
|
||||
|
||||
expect(service.addSection).toHaveBeenCalledWith(MenuID.PUBLIC, routeDataMenuSectionResolved);
|
||||
expect(service.addSection).not.toHaveBeenCalledWith(MenuID.PUBLIC, routeDataMenuChildSection);
|
||||
expect(service.addSection).toHaveBeenCalledWith(MenuID.PUBLIC, routeDataMenuOverwrittenChildSection);
|
||||
expect(service.addSection).not.toHaveBeenCalledWith(MenuID.PUBLIC, alreadyPresentMenuSection);
|
||||
expect(service.removeSection).toHaveBeenCalledWith(MenuID.PUBLIC, toBeRemovedMenuSection.id);
|
||||
});
|
||||
});
|
||||
|
||||
describe('listenForRouteChanges', () => {
|
||||
it('should build the menu sections on NavigationEnd event', () => {
|
||||
spyOn(service, 'buildRouteMenuSections');
|
||||
|
||||
service.listenForRouteChanges();
|
||||
|
||||
expect(service.buildRouteMenuSections).toHaveBeenCalledWith(MenuID.ADMIN);
|
||||
expect(service.buildRouteMenuSections).toHaveBeenCalledWith(MenuID.PUBLIC);
|
||||
});
|
||||
});
|
||||
|
||||
describe(`resolveSubstitutions`, () => {
|
||||
let linkPrefix;
|
||||
let link;
|
||||
let uuid;
|
||||
|
||||
beforeEach(() => {
|
||||
linkPrefix = 'statistics_collection_';
|
||||
link = `${linkPrefix}:id`;
|
||||
uuid = 'f7cc3ca4-3c2c-464d-8af8-add9f84f711c';
|
||||
});
|
||||
|
||||
it(`shouldn't do anything when there are no params`, () => {
|
||||
let result = (service as any).resolveSubstitutions(link, undefined);
|
||||
expect(result).toEqual(link);
|
||||
result = (service as any).resolveSubstitutions(link, null);
|
||||
expect(result).toEqual(link);
|
||||
result = (service as any).resolveSubstitutions(link, {});
|
||||
expect(result).toEqual(link);
|
||||
});
|
||||
|
||||
it(`should replace link params that are also route params`, () => {
|
||||
const result = (service as any).resolveSubstitutions(link,{ 'id': uuid });
|
||||
expect(result).toEqual(linkPrefix + uuid);
|
||||
});
|
||||
|
||||
it(`should not replace link params that aren't route params`, () => {
|
||||
const result = (service as any).resolveSubstitutions(link,{ 'something': 'else' });
|
||||
expect(result).toEqual(link);
|
||||
});
|
||||
|
||||
it(`should gracefully deal with routes that contain the name of the route param`, () => {
|
||||
const selfReferentialParam = `:id:something`;
|
||||
const result = (service as any).resolveSubstitutions(link,{ 'id': selfReferentialParam });
|
||||
expect(result).toEqual(linkPrefix + selfReferentialParam);
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
@@ -1,7 +1,6 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import {
|
||||
ActivatedRoute,
|
||||
NavigationEnd,
|
||||
Router,
|
||||
} from '@angular/router';
|
||||
import {
|
||||
@@ -16,10 +15,8 @@ import {
|
||||
} from 'rxjs';
|
||||
import {
|
||||
distinctUntilChanged,
|
||||
filter,
|
||||
map,
|
||||
switchMap,
|
||||
take,
|
||||
} from 'rxjs/operators';
|
||||
|
||||
import {
|
||||
@@ -31,7 +28,6 @@ import {
|
||||
hasNoValue,
|
||||
hasValue,
|
||||
hasValueOperator,
|
||||
isEmpty,
|
||||
isNotEmpty,
|
||||
} from '../empty.util';
|
||||
import {
|
||||
@@ -377,95 +373,4 @@ export class MenuService {
|
||||
return this.getMenuSection(menuID, id).pipe(map((section) => section.visible));
|
||||
}
|
||||
|
||||
listenForRouteChanges(): void {
|
||||
this.router.events.pipe(
|
||||
filter(event => event instanceof NavigationEnd),
|
||||
).subscribe(() => {
|
||||
Object.values(MenuID).forEach((menuID) => {
|
||||
this.buildRouteMenuSections(menuID);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Build menu sections depending on the current route
|
||||
* - Adds sections found in the current route data that aren't active yet
|
||||
* - Removes sections that are active, but not present in the current route data
|
||||
* @param menuID The menu to add/remove sections to/from
|
||||
*/
|
||||
buildRouteMenuSections(menuID: MenuID) {
|
||||
this.getNonPersistentMenuSections(menuID).pipe(
|
||||
map((sections) => sections.map((section) => section.id)),
|
||||
take(1),
|
||||
).subscribe((shouldNotPersistIDs: string[]) => {
|
||||
const resolvedSections = this.resolveRouteMenuSections(this.route.root, menuID);
|
||||
resolvedSections.forEach((section) => {
|
||||
const index = shouldNotPersistIDs.indexOf(section.id);
|
||||
if (index > -1) {
|
||||
shouldNotPersistIDs.splice(index, 1);
|
||||
} else {
|
||||
this.addSection(menuID, section);
|
||||
}
|
||||
});
|
||||
shouldNotPersistIDs.forEach((id) => {
|
||||
this.removeSection(menuID, id);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve menu sections defined in the current route data (including parent routes)
|
||||
* @param route The route to resolve data for
|
||||
* @param menuID The menu to resolve data for
|
||||
*/
|
||||
resolveRouteMenuSections(route: ActivatedRoute, menuID: MenuID): MenuSection[] {
|
||||
const data = route.snapshot.data;
|
||||
const params = route.snapshot.params;
|
||||
const last: boolean = hasNoValue(route.firstChild);
|
||||
|
||||
if (hasValue(data) && hasValue(data.menu) && hasValue(data.menu[menuID])) {
|
||||
let menuSections: MenuSection[] | MenuSection = data.menu[menuID];
|
||||
menuSections = this.resolveSubstitutions(menuSections, params);
|
||||
|
||||
if (!Array.isArray(menuSections)) {
|
||||
menuSections = [menuSections];
|
||||
}
|
||||
|
||||
if (!last) {
|
||||
const childMenuSections = this.resolveRouteMenuSections(route.firstChild, menuID);
|
||||
return [...menuSections.filter(menu => !(childMenuSections).map(childMenu => childMenu.id).includes(menu.id)), ...childMenuSections];
|
||||
} else {
|
||||
return [...menuSections];
|
||||
}
|
||||
}
|
||||
|
||||
return !last ? this.resolveRouteMenuSections(route.firstChild, menuID) : [];
|
||||
}
|
||||
|
||||
protected resolveSubstitutions(object, params) {
|
||||
let resolved;
|
||||
if (isEmpty(params)) {
|
||||
resolved = object;
|
||||
} else if (typeof object === 'string') {
|
||||
resolved = object;
|
||||
Object.entries(params).forEach(([key, value]: [string, string]) =>
|
||||
resolved = resolved.replaceAll(`:${key}`, value),
|
||||
);
|
||||
} else if (Array.isArray(object)) {
|
||||
resolved = [];
|
||||
object.forEach((entry, index) => {
|
||||
resolved[index] = this.resolveSubstitutions(object[index], params);
|
||||
});
|
||||
} else if (typeof object === 'object') {
|
||||
resolved = {};
|
||||
Object.keys(object).forEach((key) => {
|
||||
resolved[key] = this.resolveSubstitutions(object[key], params);
|
||||
});
|
||||
} else {
|
||||
resolved = object;
|
||||
}
|
||||
return resolved;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
112
src/app/shared/menu/menu.structure.spec.ts
Normal file
112
src/app/shared/menu/menu.structure.spec.ts
Normal file
@@ -0,0 +1,112 @@
|
||||
import { buildMenuStructure } from './menu.structure';
|
||||
import { MenuID } from './menu-id.model';
|
||||
import { MenuRoute } from './menu-route.model';
|
||||
import { AccessControlMenuProvider } from './providers/access-control.menu';
|
||||
import { AdminSearchMenuProvider } from './providers/admin-search.menu';
|
||||
import { BrowseMenuProvider } from './providers/browse.menu';
|
||||
import { SubscribeMenuProvider } from './providers/comcol-subscribe.menu';
|
||||
import { CommunityListMenuProvider } from './providers/community-list.menu';
|
||||
import { CurationMenuProvider } from './providers/curation.menu';
|
||||
import { DSpaceObjectEditMenuProvider } from './providers/dso-edit.menu';
|
||||
import { DsoOptionMenuProvider } from './providers/dso-option.menu';
|
||||
import { EditMenuProvider } from './providers/edit.menu';
|
||||
import { ExportMenuProvider } from './providers/export.menu';
|
||||
import { HealthMenuProvider } from './providers/health.menu';
|
||||
import { ImportMenuProvider } from './providers/import.menu';
|
||||
import { ClaimMenuProvider } from './providers/item-claim.menu';
|
||||
import { OrcidMenuProvider } from './providers/item-orcid.menu';
|
||||
import { VersioningMenuProvider } from './providers/item-versioning.menu';
|
||||
import { NewMenuProvider } from './providers/new.menu';
|
||||
import { ProcessesMenuProvider } from './providers/processes.menu';
|
||||
import { RegistriesMenuProvider } from './providers/registries.menu';
|
||||
import { StatisticsMenuProvider } from './providers/statistics.menu';
|
||||
import { SystemWideAlertMenuProvider } from './providers/system-wide-alert.menu';
|
||||
import { WorkflowMenuProvider } from './providers/workflow.menu';
|
||||
|
||||
describe('buildMenuStructure', () => {
|
||||
const providerStructure =
|
||||
{
|
||||
[MenuID.PUBLIC]: [
|
||||
CommunityListMenuProvider,
|
||||
BrowseMenuProvider,
|
||||
StatisticsMenuProvider,
|
||||
],
|
||||
[MenuID.ADMIN]: [
|
||||
NewMenuProvider,
|
||||
EditMenuProvider,
|
||||
ImportMenuProvider,
|
||||
ExportMenuProvider,
|
||||
AccessControlMenuProvider,
|
||||
AdminSearchMenuProvider,
|
||||
RegistriesMenuProvider,
|
||||
CurationMenuProvider,
|
||||
ProcessesMenuProvider,
|
||||
WorkflowMenuProvider,
|
||||
HealthMenuProvider,
|
||||
SystemWideAlertMenuProvider,
|
||||
],
|
||||
[MenuID.DSO_EDIT]: [
|
||||
DsoOptionMenuProvider.withSubs([
|
||||
SubscribeMenuProvider.onRoute(
|
||||
MenuRoute.COMMUNITY_PAGE,
|
||||
MenuRoute.COLLECTION_PAGE,
|
||||
),
|
||||
DSpaceObjectEditMenuProvider.onRoute(
|
||||
MenuRoute.COMMUNITY_PAGE,
|
||||
MenuRoute.COLLECTION_PAGE,
|
||||
MenuRoute.ITEM_PAGE,
|
||||
),
|
||||
VersioningMenuProvider.onRoute(
|
||||
MenuRoute.ITEM_PAGE,
|
||||
),
|
||||
OrcidMenuProvider.onRoute(
|
||||
MenuRoute.ITEM_PAGE,
|
||||
),
|
||||
ClaimMenuProvider.onRoute(
|
||||
MenuRoute.ITEM_PAGE,
|
||||
MenuRoute.COLLECTION_PAGE,
|
||||
),
|
||||
]),
|
||||
],
|
||||
};
|
||||
|
||||
const orderedProviderTypeList =
|
||||
[
|
||||
CommunityListMenuProvider,
|
||||
BrowseMenuProvider,
|
||||
StatisticsMenuProvider,
|
||||
NewMenuProvider,
|
||||
EditMenuProvider,
|
||||
ImportMenuProvider,
|
||||
ExportMenuProvider,
|
||||
AccessControlMenuProvider,
|
||||
AdminSearchMenuProvider,
|
||||
RegistriesMenuProvider,
|
||||
CurationMenuProvider,
|
||||
ProcessesMenuProvider,
|
||||
WorkflowMenuProvider,
|
||||
HealthMenuProvider,
|
||||
SystemWideAlertMenuProvider,
|
||||
SubscribeMenuProvider,
|
||||
DSpaceObjectEditMenuProvider,
|
||||
VersioningMenuProvider,
|
||||
OrcidMenuProvider,
|
||||
ClaimMenuProvider,
|
||||
DsoOptionMenuProvider,
|
||||
];
|
||||
|
||||
|
||||
it('should have a double amount of objects after the processing', () => {
|
||||
const result = buildMenuStructure(providerStructure);
|
||||
expect(result.length).toEqual(orderedProviderTypeList.length * 2);
|
||||
});
|
||||
|
||||
it('should return a list with a resolved provider and provider type for each provider in the provided structure', () => {
|
||||
const result = buildMenuStructure(providerStructure);
|
||||
|
||||
orderedProviderTypeList.forEach((provider, index) => {
|
||||
expect((result[index * 2] as any).deps).toEqual([provider]);
|
||||
expect(result[index * 2 + 1]).toEqual(provider);
|
||||
});
|
||||
});
|
||||
});
|
118
src/app/shared/menu/menu.structure.ts
Normal file
118
src/app/shared/menu/menu.structure.ts
Normal file
@@ -0,0 +1,118 @@
|
||||
/**
|
||||
* The contents of this file are subject to the license and copyright
|
||||
* detailed in the LICENSE and NOTICE files at the root of the source
|
||||
* tree and available online at
|
||||
*
|
||||
* http://www.dspace.org/license/
|
||||
*/
|
||||
import {
|
||||
InjectionToken,
|
||||
Provider,
|
||||
Type,
|
||||
} from '@angular/core';
|
||||
|
||||
import {
|
||||
hasValue,
|
||||
isNotEmpty,
|
||||
} from '../empty.util';
|
||||
import { MenuID } from './menu-id.model';
|
||||
import {
|
||||
AbstractMenuProvider,
|
||||
MenuProviderTypeWithOptions,
|
||||
} from './menu-provider.model';
|
||||
import { MenuRoute } from './menu-route.model';
|
||||
|
||||
export const MENU_PROVIDER = new InjectionToken<AbstractMenuProvider>('MENU_PROVIDER');
|
||||
|
||||
type MenuStructure = {
|
||||
[key in MenuID]: (Type<AbstractMenuProvider> | MenuProviderTypeWithOptions)[];
|
||||
};
|
||||
|
||||
/**
|
||||
* Builds the menu structure by converting the provider types into resolved providers
|
||||
* @param structure - The app menus structure
|
||||
*/
|
||||
export function buildMenuStructure(structure: MenuStructure): Provider[] {
|
||||
const providers: Provider[] = [
|
||||
];
|
||||
|
||||
Object.entries(structure).forEach(([menuID, providerTypes]) => {
|
||||
for (const [index, providerType] of providerTypes.entries()) {
|
||||
processProviderType(providers, menuID, providerType, index);
|
||||
}
|
||||
});
|
||||
|
||||
return providers;
|
||||
}
|
||||
|
||||
/**
|
||||
* Process a single provider type and add it to the list of providers
|
||||
* When the provider type contains paths, the paths will be added to resolved provider
|
||||
* When the provider type has sub provider, the sub providers will be processed with the current provider type as parent
|
||||
* @param providers - The list of providers
|
||||
* @param providerType - The provider to resolve and add to the list
|
||||
* @param menuID - The ID of the menu to which the provider belongs
|
||||
* @param index - The index of the provider
|
||||
* @param parentID - The ID of the parent provider if relevant
|
||||
* @param hasSubProviders - Whether this provider has sub providers
|
||||
*/
|
||||
function processProviderType(providers: Provider[], menuID: string, providerType: Type<AbstractMenuProvider> | MenuProviderTypeWithOptions, index: number, parentID?: string, hasSubProviders?: boolean) {
|
||||
if (providerType.hasOwnProperty('providerType') && providerType.hasOwnProperty('childProviderTypes')) {
|
||||
const providerPart = (providerType as any).providerType;
|
||||
const childProviderTypes = (providerType as any).childProviderTypes;
|
||||
|
||||
childProviderTypes.forEach((childProviderType, childIndex: number) => {
|
||||
processProviderType(providers, menuID, childProviderType, childIndex, `${menuID}_${index}_0`, hasSubProviders);
|
||||
});
|
||||
processProviderType(providers, menuID, providerPart, index, parentID, true);
|
||||
|
||||
} else if (providerType.hasOwnProperty('providerType') && providerType.hasOwnProperty('paths')) {
|
||||
const providerPart = (providerType as any).providerType;
|
||||
const paths = (providerType as any).paths;
|
||||
addProviderToList(providers, providerPart, menuID, index, parentID, hasSubProviders, paths);
|
||||
} else {
|
||||
addProviderToList(providers, providerType as Type<AbstractMenuProvider>, menuID, index, parentID, hasSubProviders);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves and adds a provider to a list of providers
|
||||
* @param providers - The list of providers
|
||||
* @param providerType - The provider to resolve and add to the list
|
||||
* @param menuID - The ID of the menu to which the provider belongs
|
||||
* @param index - The index of the provider
|
||||
* @param parentID - The ID of the parent provider if relevant
|
||||
* @param hasSubProviders - Whether this provider has sub providers
|
||||
* @param paths - The paths this provider should be active on if relevant
|
||||
*/
|
||||
function addProviderToList(providers: Provider[], providerType: Type<AbstractMenuProvider>, menuID: string, index: number, parentID?: string, hasSubProviders?: boolean, paths?: MenuRoute[]) {
|
||||
const resolvedProvider = {
|
||||
provide: MENU_PROVIDER,
|
||||
multi: true,
|
||||
useFactory(provider: AbstractMenuProvider): AbstractMenuProvider {
|
||||
provider.menuID = menuID as MenuID;
|
||||
provider.index = provider.index ?? index;
|
||||
if (hasValue(parentID)) {
|
||||
provider.menuProviderId = provider.menuProviderId ?? `${parentID}_${index}`;
|
||||
let providerParentID = provider.parentID;
|
||||
if (hasValue(providerParentID)) {
|
||||
providerParentID = `${providerParentID}_0`;
|
||||
}
|
||||
provider.parentID = providerParentID ?? parentID;
|
||||
} else {
|
||||
provider.menuProviderId = provider.menuProviderId ?? `${menuID}_${index}`;
|
||||
}
|
||||
if (isNotEmpty(paths)) {
|
||||
provider.activePaths = paths;
|
||||
provider.shouldPersistOnRouteChange = false;
|
||||
}
|
||||
if (hasSubProviders) {
|
||||
provider.shouldPersistOnRouteChange = false;
|
||||
}
|
||||
return provider;
|
||||
},
|
||||
deps: [providerType],
|
||||
};
|
||||
providers.push(resolvedProvider);
|
||||
providers.push(providerType);
|
||||
}
|
97
src/app/shared/menu/providers/access-control.menu.spec.ts
Normal file
97
src/app/shared/menu/providers/access-control.menu.spec.ts
Normal file
@@ -0,0 +1,97 @@
|
||||
/**
|
||||
* The contents of this file are subject to the license and copyright
|
||||
* detailed in the LICENSE and NOTICE files at the root of the source
|
||||
* tree and available online at
|
||||
*
|
||||
* http://www.dspace.org/license/
|
||||
*/
|
||||
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
import { of as observableOf } from 'rxjs';
|
||||
|
||||
import { AuthorizationDataService } from '../../../core/data/feature-authorization/authorization-data.service';
|
||||
import { FeatureID } from '../../../core/data/feature-authorization/feature-id';
|
||||
import { ScriptDataService } from '../../../core/data/processes/script-data.service';
|
||||
import { AuthorizationDataServiceStub } from '../../testing/authorization-service.stub';
|
||||
import { ScriptServiceStub } from '../../testing/script-service.stub';
|
||||
import { MenuItemType } from '../menu-item-type.model';
|
||||
import { PartialMenuSection } from '../menu-provider.model';
|
||||
import { AccessControlMenuProvider } from './access-control.menu';
|
||||
|
||||
describe('AccessControlMenuProvider', () => {
|
||||
const expectedTopSection: PartialMenuSection = {
|
||||
visible: true,
|
||||
model: {
|
||||
type: MenuItemType.TEXT,
|
||||
text: 'menu.section.access_control',
|
||||
},
|
||||
icon: 'key',
|
||||
};
|
||||
|
||||
const expectedSubSections: PartialMenuSection[] = [
|
||||
{
|
||||
visible: true,
|
||||
model: {
|
||||
type: MenuItemType.LINK,
|
||||
text: 'menu.section.access_control_people',
|
||||
link: '/access-control/epeople',
|
||||
},
|
||||
},
|
||||
{
|
||||
visible: false,
|
||||
model: {
|
||||
type: MenuItemType.LINK,
|
||||
text: 'menu.section.access_control_groups',
|
||||
link: '/access-control/groups',
|
||||
},
|
||||
},
|
||||
{
|
||||
visible: true,
|
||||
model: {
|
||||
type: MenuItemType.LINK,
|
||||
text: 'menu.section.access_control_bulk',
|
||||
link: '/access-control/bulk-access',
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
let provider: AccessControlMenuProvider;
|
||||
let authorizationServiceStub = new AuthorizationDataServiceStub();
|
||||
|
||||
beforeEach(() => {
|
||||
spyOn(authorizationServiceStub, 'isAuthorized').and.callFake((id: FeatureID) => {
|
||||
if (id === FeatureID.CanManageGroups) {
|
||||
return observableOf(false);
|
||||
} else {
|
||||
return observableOf(true);
|
||||
}
|
||||
});
|
||||
|
||||
TestBed.configureTestingModule({
|
||||
providers: [
|
||||
AccessControlMenuProvider,
|
||||
{ provide: AuthorizationDataService, useValue: authorizationServiceStub },
|
||||
{ provide: ScriptDataService, useClass: ScriptServiceStub },
|
||||
],
|
||||
});
|
||||
provider = TestBed.inject(AccessControlMenuProvider);
|
||||
});
|
||||
|
||||
it('should be created', () => {
|
||||
expect(provider).toBeTruthy();
|
||||
});
|
||||
|
||||
it('getTopSection should return expected menu section', (done) => {
|
||||
provider.getTopSection().subscribe((section) => {
|
||||
expect(section).toEqual(expectedTopSection);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('getSubSections should return expected menu sections', (done) => {
|
||||
provider.getSubSections().subscribe((sections) => {
|
||||
expect(sections).toEqual(expectedSubSections);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
85
src/app/shared/menu/providers/access-control.menu.ts
Normal file
85
src/app/shared/menu/providers/access-control.menu.ts
Normal file
@@ -0,0 +1,85 @@
|
||||
/**
|
||||
* The contents of this file are subject to the license and copyright
|
||||
* detailed in the LICENSE and NOTICE files at the root of the source
|
||||
* tree and available online at
|
||||
*
|
||||
* http://www.dspace.org/license/
|
||||
*/
|
||||
|
||||
import { Injectable } from '@angular/core';
|
||||
import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
|
||||
import {
|
||||
combineLatest as observableCombineLatest,
|
||||
map,
|
||||
Observable,
|
||||
of as observableOf,
|
||||
} from 'rxjs';
|
||||
|
||||
import { AuthorizationDataService } from '../../../core/data/feature-authorization/authorization-data.service';
|
||||
import { FeatureID } from '../../../core/data/feature-authorization/feature-id';
|
||||
import { ScriptDataService } from '../../../core/data/processes/script-data.service';
|
||||
import { MenuItemType } from '../menu-item-type.model';
|
||||
import { PartialMenuSection } from '../menu-provider.model';
|
||||
import { AbstractExpandableMenuProvider } from './helper-providers/expandable-menu-provider';
|
||||
|
||||
/**
|
||||
* Menu provider to create the "Access Control" menu (and subsections) in the admin sidebar
|
||||
*/
|
||||
@Injectable()
|
||||
export class AccessControlMenuProvider extends AbstractExpandableMenuProvider {
|
||||
|
||||
constructor(
|
||||
protected authorizationService: AuthorizationDataService,
|
||||
protected scriptDataService: ScriptDataService,
|
||||
protected modalService: NgbModal,
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
public getTopSection(): Observable<PartialMenuSection> {
|
||||
return observableOf({
|
||||
model: {
|
||||
type: MenuItemType.TEXT,
|
||||
text: 'menu.section.access_control',
|
||||
},
|
||||
icon: 'key',
|
||||
visible: true,
|
||||
});
|
||||
}
|
||||
|
||||
public getSubSections(): Observable<PartialMenuSection[]> {
|
||||
return observableCombineLatest([
|
||||
this.authorizationService.isAuthorized(FeatureID.AdministratorOf),
|
||||
this.authorizationService.isAuthorized(FeatureID.CanManageGroups),
|
||||
]).pipe(
|
||||
map(([isSiteAdmin, canManageGroups]: [boolean, boolean]) => {
|
||||
return [
|
||||
{
|
||||
visible: isSiteAdmin,
|
||||
model: {
|
||||
type: MenuItemType.LINK,
|
||||
text: 'menu.section.access_control_people',
|
||||
link: '/access-control/epeople',
|
||||
},
|
||||
},
|
||||
{
|
||||
visible: canManageGroups,
|
||||
model: {
|
||||
type: MenuItemType.LINK,
|
||||
text: 'menu.section.access_control_groups',
|
||||
link: '/access-control/groups',
|
||||
},
|
||||
},
|
||||
{
|
||||
visible: isSiteAdmin,
|
||||
model: {
|
||||
type: MenuItemType.LINK,
|
||||
text: 'menu.section.access_control_bulk',
|
||||
link: '/access-control/bulk-access',
|
||||
},
|
||||
},
|
||||
];
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
60
src/app/shared/menu/providers/admin-search.menu.spec.ts
Normal file
60
src/app/shared/menu/providers/admin-search.menu.spec.ts
Normal file
@@ -0,0 +1,60 @@
|
||||
/**
|
||||
* The contents of this file are subject to the license and copyright
|
||||
* detailed in the LICENSE and NOTICE files at the root of the source
|
||||
* tree and available online at
|
||||
*
|
||||
* http://www.dspace.org/license/
|
||||
*/
|
||||
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
import { of as observableOf } from 'rxjs';
|
||||
|
||||
import { AuthorizationDataService } from '../../../core/data/feature-authorization/authorization-data.service';
|
||||
import { AuthorizationDataServiceStub } from '../../testing/authorization-service.stub';
|
||||
import { MenuItemType } from '../menu-item-type.model';
|
||||
import { PartialMenuSection } from '../menu-provider.model';
|
||||
import { AdminSearchMenuProvider } from './admin-search.menu';
|
||||
|
||||
describe('AdminSearchMenuProvider', () => {
|
||||
|
||||
const expectedSections: PartialMenuSection[] = [
|
||||
{
|
||||
visible: true,
|
||||
model: {
|
||||
type: MenuItemType.LINK,
|
||||
text: 'menu.section.admin_search',
|
||||
link: '/admin/search',
|
||||
},
|
||||
icon: 'search',
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
let provider: AdminSearchMenuProvider;
|
||||
let authorizationServiceStub = new AuthorizationDataServiceStub();
|
||||
|
||||
beforeEach(() => {
|
||||
spyOn(authorizationServiceStub, 'isAuthorized').and.returnValue(
|
||||
observableOf(true),
|
||||
);
|
||||
|
||||
TestBed.configureTestingModule({
|
||||
providers: [
|
||||
AdminSearchMenuProvider,
|
||||
{ provide: AuthorizationDataService, useValue: authorizationServiceStub },
|
||||
],
|
||||
});
|
||||
provider = TestBed.inject(AdminSearchMenuProvider);
|
||||
});
|
||||
|
||||
it('should be created', () => {
|
||||
expect(provider).toBeTruthy();
|
||||
});
|
||||
|
||||
it('getSections should return expected menu sections', (done) => {
|
||||
provider.getSections().subscribe((sections) => {
|
||||
expect(sections).toEqual(expectedSections);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
51
src/app/shared/menu/providers/admin-search.menu.ts
Normal file
51
src/app/shared/menu/providers/admin-search.menu.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
/**
|
||||
* The contents of this file are subject to the license and copyright
|
||||
* detailed in the LICENSE and NOTICE files at the root of the source
|
||||
* tree and available online at
|
||||
*
|
||||
* http://www.dspace.org/license/
|
||||
*/
|
||||
|
||||
import { Injectable } from '@angular/core';
|
||||
import {
|
||||
map,
|
||||
Observable,
|
||||
} from 'rxjs';
|
||||
|
||||
import { AuthorizationDataService } from '../../../core/data/feature-authorization/authorization-data.service';
|
||||
import { FeatureID } from '../../../core/data/feature-authorization/feature-id';
|
||||
import { MenuItemType } from '../menu-item-type.model';
|
||||
import {
|
||||
AbstractMenuProvider,
|
||||
PartialMenuSection,
|
||||
} from '../menu-provider.model';
|
||||
|
||||
/**
|
||||
* Menu provider to create the "Admin Search" menu in the admin sidebar
|
||||
*/
|
||||
@Injectable()
|
||||
export class AdminSearchMenuProvider extends AbstractMenuProvider {
|
||||
constructor(
|
||||
protected authorizationService: AuthorizationDataService,
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
public getSections(): Observable<PartialMenuSection[]> {
|
||||
return this.authorizationService.isAuthorized(FeatureID.AdministratorOf).pipe(
|
||||
map((isSiteAdmin) => {
|
||||
return [
|
||||
{
|
||||
visible: isSiteAdmin,
|
||||
model: {
|
||||
type: MenuItemType.LINK,
|
||||
text: 'menu.section.admin_search',
|
||||
link: '/admin/search',
|
||||
},
|
||||
icon: 'search',
|
||||
},
|
||||
];
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
112
src/app/shared/menu/providers/browse.menu.spec.ts
Normal file
112
src/app/shared/menu/providers/browse.menu.spec.ts
Normal file
@@ -0,0 +1,112 @@
|
||||
/**
|
||||
* The contents of this file are subject to the license and copyright
|
||||
* detailed in the LICENSE and NOTICE files at the root of the source
|
||||
* tree and available online at
|
||||
*
|
||||
* http://www.dspace.org/license/
|
||||
*/
|
||||
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
|
||||
import {
|
||||
APP_CONFIG,
|
||||
AppConfig,
|
||||
} from '../../../../config/app-config.interface';
|
||||
import { BrowseService } from '../../../core/browse/browse.service';
|
||||
import { ObjectCacheService } from '../../../core/cache/object-cache.service';
|
||||
import { BrowseDefinition } from '../../../core/shared/browse-definition.model';
|
||||
import { getMockObjectCacheService } from '../../mocks/object-cache.service.mock';
|
||||
import { createSuccessfulRemoteDataObject$ } from '../../remote-data.utils';
|
||||
import { BrowseServiceStub } from '../../testing/browse-service.stub';
|
||||
import { createPaginatedList } from '../../testing/utils.test';
|
||||
import { MenuItemType } from '../menu-item-type.model';
|
||||
import { PartialMenuSection } from '../menu-provider.model';
|
||||
import { BrowseMenuProvider } from './browse.menu';
|
||||
|
||||
describe('BrowseMenuProvider', () => {
|
||||
|
||||
const expectedTopSection: PartialMenuSection = {
|
||||
visible: true,
|
||||
model: {
|
||||
type: MenuItemType.TEXT,
|
||||
text: 'menu.section.browse_global',
|
||||
},
|
||||
icon: 'globe',
|
||||
};
|
||||
|
||||
const expectedSubSections: (enableMapLink: boolean) => PartialMenuSection[] = (enableMapLink) => [
|
||||
{
|
||||
visible: true,
|
||||
model: {
|
||||
type: MenuItemType.LINK,
|
||||
text: 'menu.section.browse_global_by_author',
|
||||
link: '/browse/author',
|
||||
},
|
||||
},
|
||||
{
|
||||
visible: true,
|
||||
model: {
|
||||
type: MenuItemType.LINK,
|
||||
text: 'menu.section.browse_global_by_subject',
|
||||
link: '/browse/subject',
|
||||
},
|
||||
},
|
||||
{
|
||||
visible: enableMapLink,
|
||||
model: {
|
||||
type: MenuItemType.LINK,
|
||||
text: `menu.section.browse_global_geospatial_map`,
|
||||
link: `/browse/map`,
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
let provider: BrowseMenuProvider;
|
||||
let browseServiceStub = BrowseServiceStub;
|
||||
|
||||
beforeEach(() => {
|
||||
spyOn(browseServiceStub, 'getBrowseDefinitions').and.returnValue(
|
||||
createSuccessfulRemoteDataObject$(createPaginatedList([
|
||||
{ id: 'author' } as BrowseDefinition,
|
||||
{ id: 'subject' } as BrowseDefinition,
|
||||
])),
|
||||
);
|
||||
|
||||
TestBed.configureTestingModule({
|
||||
providers: [
|
||||
BrowseMenuProvider,
|
||||
{ provide: BrowseService, useValue: browseServiceStub },
|
||||
{ provide: ObjectCacheService, useValue: getMockObjectCacheService() },
|
||||
{ provide: APP_CONFIG, useValue: { geospatialMapViewer: { enableBrowseMap: true } } as AppConfig },
|
||||
],
|
||||
});
|
||||
provider = TestBed.inject(BrowseMenuProvider);
|
||||
});
|
||||
|
||||
it('should be created', () => {
|
||||
expect(provider).toBeTruthy();
|
||||
});
|
||||
|
||||
it('getTopSection should return expected menu section', (done) => {
|
||||
provider.getTopSection().subscribe((section) => {
|
||||
expect(section).toEqual(expectedTopSection);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('getSubSections should return expected menu sections', (done) => {
|
||||
provider.getSubSections().subscribe((sections) => {
|
||||
expect(sections).toEqual(expectedSubSections(true));
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('getSubSections should return expected menu sections', (done) => {
|
||||
TestBed.inject(APP_CONFIG).geospatialMapViewer.enableBrowseMap = false;
|
||||
provider.getSubSections().subscribe((sections) => {
|
||||
expect(sections).toEqual(expectedSubSections(false));
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
88
src/app/shared/menu/providers/browse.menu.ts
Normal file
88
src/app/shared/menu/providers/browse.menu.ts
Normal file
@@ -0,0 +1,88 @@
|
||||
/**
|
||||
* The contents of this file are subject to the license and copyright
|
||||
* detailed in the LICENSE and NOTICE files at the root of the source
|
||||
* tree and available online at
|
||||
*
|
||||
* http://www.dspace.org/license/
|
||||
*/
|
||||
|
||||
import {
|
||||
Inject,
|
||||
Injectable,
|
||||
} from '@angular/core';
|
||||
import {
|
||||
Observable,
|
||||
of as observableOf,
|
||||
} from 'rxjs';
|
||||
import { map } from 'rxjs/operators';
|
||||
|
||||
import {
|
||||
APP_CONFIG,
|
||||
AppConfig,
|
||||
} from '../../../../config/app-config.interface';
|
||||
import { BrowseService } from '../../../core/browse/browse.service';
|
||||
import { PaginatedList } from '../../../core/data/paginated-list.model';
|
||||
import { RemoteData } from '../../../core/data/remote-data';
|
||||
import { BrowseDefinition } from '../../../core/shared/browse-definition.model';
|
||||
import { getFirstSucceededRemoteData } from '../../../core/shared/operators';
|
||||
import { TextMenuItemModel } from '../menu-item/models/text.model';
|
||||
import { MenuItemType } from '../menu-item-type.model';
|
||||
import { PartialMenuSection } from '../menu-provider.model';
|
||||
import { AbstractExpandableMenuProvider } from './helper-providers/expandable-menu-provider';
|
||||
|
||||
/**
|
||||
* Menu provider to create the "All of DSpace" browse menu sections in the public navbar
|
||||
*/
|
||||
@Injectable()
|
||||
export class BrowseMenuProvider extends AbstractExpandableMenuProvider {
|
||||
constructor(
|
||||
@Inject(APP_CONFIG) protected appConfig: AppConfig,
|
||||
protected browseService: BrowseService,
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
getTopSection(): Observable<PartialMenuSection> {
|
||||
return observableOf(
|
||||
{
|
||||
model: {
|
||||
type: MenuItemType.TEXT,
|
||||
text: 'menu.section.browse_global',
|
||||
} as TextMenuItemModel,
|
||||
icon: 'globe',
|
||||
visible: true,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves subsections by fetching the browse definitions from the backend and mapping them to partial menu sections.
|
||||
*/
|
||||
getSubSections(): Observable<PartialMenuSection[]> {
|
||||
return this.browseService.getBrowseDefinitions().pipe(
|
||||
getFirstSucceededRemoteData(),
|
||||
map((rd: RemoteData<PaginatedList<BrowseDefinition>>) => {
|
||||
return [
|
||||
...rd.payload.page.map((browseDef) => {
|
||||
return {
|
||||
visible: true,
|
||||
model: {
|
||||
type: MenuItemType.LINK,
|
||||
text: `menu.section.browse_global_by_${browseDef.id}`,
|
||||
link: `/browse/${browseDef.id}`,
|
||||
},
|
||||
};
|
||||
}),
|
||||
{
|
||||
visible: this.appConfig.geospatialMapViewer.enableBrowseMap,
|
||||
model: {
|
||||
type: MenuItemType.LINK,
|
||||
text: `menu.section.browse_global_geospatial_map`,
|
||||
link: `/browse/map`,
|
||||
},
|
||||
},
|
||||
];
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
83
src/app/shared/menu/providers/coar-notify.menu.spec.ts
Normal file
83
src/app/shared/menu/providers/coar-notify.menu.spec.ts
Normal file
@@ -0,0 +1,83 @@
|
||||
/**
|
||||
* The contents of this file are subject to the license and copyright
|
||||
* detailed in the LICENSE and NOTICE files at the root of the source
|
||||
* tree and available online at
|
||||
*
|
||||
* http://www.dspace.org/license/
|
||||
*/
|
||||
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
import { of as observableOf } from 'rxjs';
|
||||
|
||||
import { AuthorizationDataService } from '../../../core/data/feature-authorization/authorization-data.service';
|
||||
import { AuthorizationDataServiceStub } from '../../testing/authorization-service.stub';
|
||||
import { LinkMenuItemModel } from '../menu-item/models/link.model';
|
||||
import { TextMenuItemModel } from '../menu-item/models/text.model';
|
||||
import { MenuItemType } from '../menu-item-type.model';
|
||||
import { PartialMenuSection } from '../menu-provider.model';
|
||||
import { CoarNotifyMenuProvider } from './coar-notify.menu';
|
||||
|
||||
describe('CoarNotifyMenuProvider', () => {
|
||||
const expectedTopSection: PartialMenuSection = {
|
||||
visible: true,
|
||||
model: {
|
||||
type: MenuItemType.TEXT,
|
||||
text: 'menu.section.coar_notify',
|
||||
} as TextMenuItemModel,
|
||||
icon: 'inbox',
|
||||
};
|
||||
|
||||
const expectedSubSections: PartialMenuSection[] = [
|
||||
{
|
||||
visible: true,
|
||||
model: {
|
||||
type: MenuItemType.LINK,
|
||||
text: 'menu.section.notify_dashboard',
|
||||
link: '/admin/notify-dashboard',
|
||||
} as LinkMenuItemModel,
|
||||
},
|
||||
{
|
||||
visible: true,
|
||||
model: {
|
||||
type: MenuItemType.LINK,
|
||||
text: 'menu.section.services',
|
||||
link: '/admin/ldn/services',
|
||||
} as LinkMenuItemModel,
|
||||
},
|
||||
];
|
||||
|
||||
let provider: CoarNotifyMenuProvider;
|
||||
let authorizationServiceStub = new AuthorizationDataServiceStub();
|
||||
|
||||
beforeEach(() => {
|
||||
|
||||
spyOn(authorizationServiceStub, 'isAuthorized').and.returnValue(
|
||||
observableOf(true),
|
||||
);
|
||||
TestBed.configureTestingModule({
|
||||
providers: [
|
||||
CoarNotifyMenuProvider,
|
||||
{ provide: AuthorizationDataService, useValue: authorizationServiceStub },
|
||||
],
|
||||
});
|
||||
provider = TestBed.inject(CoarNotifyMenuProvider);
|
||||
});
|
||||
|
||||
it('should be created', () => {
|
||||
expect(provider).toBeTruthy();
|
||||
});
|
||||
|
||||
it('getTopSection should return expected menu section', (done) => {
|
||||
provider.getTopSection().subscribe((section) => {
|
||||
expect(section).toEqual(expectedTopSection);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('getSubSections should return expected menu sections', (done) => {
|
||||
provider.getSubSections().subscribe((sections) => {
|
||||
expect(sections).toEqual(expectedSubSections);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
77
src/app/shared/menu/providers/coar-notify.menu.ts
Normal file
77
src/app/shared/menu/providers/coar-notify.menu.ts
Normal file
@@ -0,0 +1,77 @@
|
||||
/**
|
||||
* The contents of this file are subject to the license and copyright
|
||||
* detailed in the LICENSE and NOTICE files at the root of the source
|
||||
* tree and available online at
|
||||
*
|
||||
* http://www.dspace.org/license/
|
||||
*/
|
||||
|
||||
import { Injectable } from '@angular/core';
|
||||
import {
|
||||
combineLatest as observableCombineLatest,
|
||||
Observable,
|
||||
} from 'rxjs';
|
||||
import { map } from 'rxjs/operators';
|
||||
|
||||
import { AuthorizationDataService } from '../../../core/data/feature-authorization/authorization-data.service';
|
||||
import { FeatureID } from '../../../core/data/feature-authorization/feature-id';
|
||||
import { LinkMenuItemModel } from '../menu-item/models/link.model';
|
||||
import { TextMenuItemModel } from '../menu-item/models/text.model';
|
||||
import { MenuItemType } from '../menu-item-type.model';
|
||||
import { PartialMenuSection } from '../menu-provider.model';
|
||||
import { AbstractExpandableMenuProvider } from './helper-providers/expandable-menu-provider';
|
||||
|
||||
/**
|
||||
* Menu provider to create the "COAR Notify" menu (and subsections) in the admin sidebar
|
||||
*/
|
||||
@Injectable()
|
||||
export class CoarNotifyMenuProvider extends AbstractExpandableMenuProvider {
|
||||
constructor(
|
||||
protected authorizationService: AuthorizationDataService,
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
getSubSections(): Observable<PartialMenuSection[]> {
|
||||
return observableCombineLatest([
|
||||
this.authorizationService.isAuthorized(FeatureID.CoarNotifyEnabled),
|
||||
this.authorizationService.isAuthorized(FeatureID.AdministratorOf),
|
||||
]).pipe(
|
||||
map(([isCoarNotifyEnabled, isSiteAdmin]: [boolean, boolean]) => {
|
||||
return [{
|
||||
visible: isSiteAdmin && isCoarNotifyEnabled,
|
||||
model: {
|
||||
type: MenuItemType.LINK,
|
||||
text: 'menu.section.notify_dashboard',
|
||||
link: '/admin/notify-dashboard',
|
||||
} as LinkMenuItemModel,
|
||||
},
|
||||
/* LDN Services */
|
||||
{
|
||||
visible: isSiteAdmin && isCoarNotifyEnabled,
|
||||
model: {
|
||||
type: MenuItemType.LINK,
|
||||
text: 'menu.section.services',
|
||||
link: '/admin/ldn/services',
|
||||
} as LinkMenuItemModel,
|
||||
}];
|
||||
}));
|
||||
}
|
||||
|
||||
getTopSection(): Observable<PartialMenuSection> {
|
||||
return observableCombineLatest([
|
||||
this.authorizationService.isAuthorized(FeatureID.CoarNotifyEnabled),
|
||||
this.authorizationService.isAuthorized(FeatureID.AdministratorOf),
|
||||
]).pipe(
|
||||
map(([isCoarNotifyEnabled, isSiteAdmin]: [boolean, boolean]) => {
|
||||
return {
|
||||
visible: isSiteAdmin && isCoarNotifyEnabled,
|
||||
model: {
|
||||
type: MenuItemType.TEXT,
|
||||
text: 'menu.section.coar_notify',
|
||||
} as TextMenuItemModel,
|
||||
icon: 'inbox',
|
||||
};
|
||||
}));
|
||||
}
|
||||
}
|
65
src/app/shared/menu/providers/comcol-subscribe.menu.spec.ts
Normal file
65
src/app/shared/menu/providers/comcol-subscribe.menu.spec.ts
Normal file
@@ -0,0 +1,65 @@
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
|
||||
import { of as observableOf } from 'rxjs';
|
||||
|
||||
import { AuthorizationDataService } from '../../../core/data/feature-authorization/authorization-data.service';
|
||||
import { Collection } from '../../../core/shared/collection.model';
|
||||
import { MenuItemType } from '../menu-item-type.model';
|
||||
import { PartialMenuSection } from '../menu-provider.model';
|
||||
import { SubscribeMenuProvider } from './comcol-subscribe.menu';
|
||||
|
||||
describe('SubscribeMenuProvider', () => {
|
||||
|
||||
const expectedSections: PartialMenuSection[] = [
|
||||
{
|
||||
visible: true,
|
||||
model: {
|
||||
type: MenuItemType.ONCLICK,
|
||||
text: 'subscriptions.tooltip',
|
||||
function: jasmine.any(Function) as any,
|
||||
},
|
||||
icon: 'bell',
|
||||
},
|
||||
];
|
||||
|
||||
let provider: SubscribeMenuProvider;
|
||||
|
||||
const dso: Collection = Object.assign(new Collection(), { _links: { self: { href: 'self-link' } } });
|
||||
|
||||
|
||||
let authorizationService;
|
||||
let modalService;
|
||||
|
||||
beforeEach(() => {
|
||||
|
||||
authorizationService = jasmine.createSpyObj('authorizationService', {
|
||||
'isAuthorized': observableOf(true),
|
||||
});
|
||||
|
||||
modalService = jasmine.createSpyObj('modalService', ['open']);
|
||||
|
||||
TestBed.configureTestingModule({
|
||||
providers: [
|
||||
SubscribeMenuProvider,
|
||||
{ provide: AuthorizationDataService, useValue: authorizationService },
|
||||
{ provide: NgbModal, useValue: modalService },
|
||||
],
|
||||
});
|
||||
provider = TestBed.inject(SubscribeMenuProvider);
|
||||
});
|
||||
|
||||
it('should be created', () => {
|
||||
expect(provider).toBeTruthy();
|
||||
});
|
||||
|
||||
describe('getSectionsForContext', () => {
|
||||
it('should return the expected sections', (done) => {
|
||||
provider.getSectionsForContext(dso).subscribe((sections) => {
|
||||
expect(sections).toEqual(expectedSections);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
});
|
59
src/app/shared/menu/providers/comcol-subscribe.menu.ts
Normal file
59
src/app/shared/menu/providers/comcol-subscribe.menu.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
/**
|
||||
* The contents of this file are subject to the license and copyright
|
||||
* detailed in the LICENSE and NOTICE files at the root of the source
|
||||
* tree and available online at
|
||||
*
|
||||
* http://www.dspace.org/license/
|
||||
*/
|
||||
import { Injectable } from '@angular/core';
|
||||
import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
|
||||
import {
|
||||
combineLatest,
|
||||
Observable,
|
||||
} from 'rxjs';
|
||||
import { map } from 'rxjs/operators';
|
||||
|
||||
import { AuthorizationDataService } from '../../../core/data/feature-authorization/authorization-data.service';
|
||||
import { FeatureID } from '../../../core/data/feature-authorization/feature-id';
|
||||
import { DSpaceObject } from '../../../core/shared/dspace-object.model';
|
||||
import { SubscriptionModalComponent } from '../../subscriptions/subscription-modal/subscription-modal.component';
|
||||
import { OnClickMenuItemModel } from '../menu-item/models/onclick.model';
|
||||
import { MenuItemType } from '../menu-item-type.model';
|
||||
import { PartialMenuSection } from '../menu-provider.model';
|
||||
import { DSpaceObjectPageMenuProvider } from './helper-providers/dso.menu';
|
||||
|
||||
/**
|
||||
* Menu provider to create the "Subscribe" option in the DSO edit menu
|
||||
*/
|
||||
@Injectable()
|
||||
export class SubscribeMenuProvider extends DSpaceObjectPageMenuProvider {
|
||||
constructor(
|
||||
protected authorizationService: AuthorizationDataService,
|
||||
protected modalService: NgbModal,
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
public getSectionsForContext(dso: DSpaceObject): Observable<PartialMenuSection[]> {
|
||||
return combineLatest([
|
||||
this.authorizationService.isAuthorized(FeatureID.CanSubscribe, dso.self),
|
||||
]).pipe(
|
||||
map(([canSubscribe]) => {
|
||||
return [
|
||||
{
|
||||
visible: canSubscribe,
|
||||
model: {
|
||||
type: MenuItemType.ONCLICK,
|
||||
text: 'subscriptions.tooltip',
|
||||
function: () => {
|
||||
const modalRef = this.modalService.open(SubscriptionModalComponent);
|
||||
modalRef.componentInstance.dso = dso;
|
||||
},
|
||||
} as OnClickMenuItemModel,
|
||||
icon: 'bell',
|
||||
},
|
||||
] as PartialMenuSection[];
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
49
src/app/shared/menu/providers/community-list.menu.spec.ts
Normal file
49
src/app/shared/menu/providers/community-list.menu.spec.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
/**
|
||||
* The contents of this file are subject to the license and copyright
|
||||
* detailed in the LICENSE and NOTICE files at the root of the source
|
||||
* tree and available online at
|
||||
*
|
||||
* http://www.dspace.org/license/
|
||||
*/
|
||||
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
|
||||
import { MenuItemType } from '../menu-item-type.model';
|
||||
import { PartialMenuSection } from '../menu-provider.model';
|
||||
import { CommunityListMenuProvider } from './community-list.menu';
|
||||
|
||||
describe('CommunityListMenuProvider', () => {
|
||||
const expectedSections: PartialMenuSection[] = [
|
||||
{
|
||||
visible: true,
|
||||
model: {
|
||||
type: MenuItemType.LINK,
|
||||
text: `menu.section.browse_global_communities_and_collections`,
|
||||
link: `/community-list`,
|
||||
},
|
||||
icon: 'diagram-project',
|
||||
},
|
||||
];
|
||||
|
||||
let provider: CommunityListMenuProvider;
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
providers: [
|
||||
CommunityListMenuProvider,
|
||||
],
|
||||
});
|
||||
provider = TestBed.inject(CommunityListMenuProvider);
|
||||
});
|
||||
|
||||
it('should be created', () => {
|
||||
expect(provider).toBeTruthy();
|
||||
});
|
||||
|
||||
it('getSections should return expected menu sections', (done) => {
|
||||
provider.getSections().subscribe((sections) => {
|
||||
expect(sections).toEqual(expectedSections);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
39
src/app/shared/menu/providers/community-list.menu.ts
Normal file
39
src/app/shared/menu/providers/community-list.menu.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
/**
|
||||
* The contents of this file are subject to the license and copyright
|
||||
* detailed in the LICENSE and NOTICE files at the root of the source
|
||||
* tree and available online at
|
||||
*
|
||||
* http://www.dspace.org/license/
|
||||
*/
|
||||
|
||||
import { Injectable } from '@angular/core';
|
||||
import {
|
||||
Observable,
|
||||
of,
|
||||
} from 'rxjs';
|
||||
|
||||
import { MenuItemType } from '../menu-item-type.model';
|
||||
import {
|
||||
AbstractMenuProvider,
|
||||
PartialMenuSection,
|
||||
} from '../menu-provider.model';
|
||||
|
||||
/**
|
||||
* Menu provider to create the "Communities & Collections" menu section in the public navbar
|
||||
*/
|
||||
@Injectable()
|
||||
export class CommunityListMenuProvider extends AbstractMenuProvider {
|
||||
public getSections(): Observable<PartialMenuSection[]> {
|
||||
return of([
|
||||
{
|
||||
visible: true,
|
||||
model: {
|
||||
type: MenuItemType.LINK,
|
||||
text: `menu.section.browse_global_communities_and_collections`,
|
||||
link: `/community-list`,
|
||||
},
|
||||
icon: 'diagram-project',
|
||||
},
|
||||
] as PartialMenuSection[]);
|
||||
}
|
||||
}
|
100
src/app/shared/menu/providers/create-report.menu.spec.ts
Normal file
100
src/app/shared/menu/providers/create-report.menu.spec.ts
Normal file
@@ -0,0 +1,100 @@
|
||||
/**
|
||||
* The contents of this file are subject to the license and copyright
|
||||
* detailed in the LICENSE and NOTICE files at the root of the source
|
||||
* tree and available online at
|
||||
*
|
||||
* http://www.dspace.org/license/
|
||||
*/
|
||||
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
import { of as observableOf } from 'rxjs';
|
||||
|
||||
import { ConfigurationDataService } from '../../../core/data/configuration-data.service';
|
||||
import { AuthorizationDataService } from '../../../core/data/feature-authorization/authorization-data.service';
|
||||
import { FeatureID } from '../../../core/data/feature-authorization/feature-id';
|
||||
import { ConfigurationProperty } from '../../../core/shared/configuration-property.model';
|
||||
import { createSuccessfulRemoteDataObject$ } from '../../remote-data.utils';
|
||||
import { AuthorizationDataServiceStub } from '../../testing/authorization-service.stub';
|
||||
import { ConfigurationDataServiceStub } from '../../testing/configuration-data.service.stub';
|
||||
import { LinkMenuItemModel } from '../menu-item/models/link.model';
|
||||
import { TextMenuItemModel } from '../menu-item/models/text.model';
|
||||
import { MenuItemType } from '../menu-item-type.model';
|
||||
import { PartialMenuSection } from '../menu-provider.model';
|
||||
import { CreateReportMenuProvider } from './create-report.menu';
|
||||
|
||||
describe('CreateReportMenuProvider', () => {
|
||||
const expectedTopSection: PartialMenuSection = {
|
||||
visible: true,
|
||||
model: {
|
||||
type: MenuItemType.TEXT,
|
||||
text: 'menu.section.reports',
|
||||
} as TextMenuItemModel,
|
||||
icon: 'file-alt',
|
||||
};
|
||||
|
||||
const expectedSubSections: PartialMenuSection[] = [
|
||||
{
|
||||
visible: true,
|
||||
model: {
|
||||
type: MenuItemType.LINK,
|
||||
text: 'menu.section.reports.collections',
|
||||
link: '/admin/reports/collections',
|
||||
} as LinkMenuItemModel,
|
||||
icon: 'user-check',
|
||||
},
|
||||
{
|
||||
visible: true,
|
||||
model: {
|
||||
type: MenuItemType.LINK,
|
||||
text: 'menu.section.reports.queries',
|
||||
link: '/admin/reports/queries',
|
||||
} as LinkMenuItemModel,
|
||||
icon: 'user-check',
|
||||
},
|
||||
];
|
||||
|
||||
let provider: CreateReportMenuProvider;
|
||||
let authorizationServiceStub = new AuthorizationDataServiceStub();
|
||||
let configurationDataService = new ConfigurationDataServiceStub();
|
||||
|
||||
beforeEach(() => {
|
||||
spyOn(authorizationServiceStub, 'isAuthorized').and.callFake((id: FeatureID) => {
|
||||
if (id === FeatureID.AdministratorOf) {
|
||||
return observableOf(true);
|
||||
} else {
|
||||
return observableOf(false);
|
||||
}
|
||||
});
|
||||
|
||||
spyOn(configurationDataService, 'findByPropertyName').and.callFake((property: string) => {
|
||||
return createSuccessfulRemoteDataObject$(Object.assign({}, new ConfigurationProperty(), { values: ['true'] }));
|
||||
});
|
||||
|
||||
TestBed.configureTestingModule({
|
||||
providers: [
|
||||
CreateReportMenuProvider,
|
||||
{ provide: AuthorizationDataService, useValue: authorizationServiceStub },
|
||||
{ provide: ConfigurationDataService, useValue: configurationDataService },
|
||||
],
|
||||
});
|
||||
provider = TestBed.inject(CreateReportMenuProvider);
|
||||
});
|
||||
|
||||
it('should be created', () => {
|
||||
expect(provider).toBeTruthy();
|
||||
});
|
||||
|
||||
it('getTopSection should return expected menu section', (done) => {
|
||||
provider.getTopSection().subscribe((section) => {
|
||||
expect(section).toEqual(expectedTopSection);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('getSubSections should return expected menu sections', (done) => {
|
||||
provider.getSubSections().subscribe((sections) => {
|
||||
expect(sections).toEqual(expectedSubSections);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
93
src/app/shared/menu/providers/create-report.menu.ts
Normal file
93
src/app/shared/menu/providers/create-report.menu.ts
Normal file
@@ -0,0 +1,93 @@
|
||||
/**
|
||||
* The contents of this file are subject to the license and copyright
|
||||
* detailed in the LICENSE and NOTICE files at the root of the source
|
||||
* tree and available online at
|
||||
*
|
||||
* http://www.dspace.org/license/
|
||||
*/
|
||||
|
||||
import { Injectable } from '@angular/core';
|
||||
import {
|
||||
combineLatest as observableCombineLatest,
|
||||
Observable,
|
||||
} from 'rxjs';
|
||||
import { map } from 'rxjs/operators';
|
||||
|
||||
import { ConfigurationDataService } from '../../../core/data/configuration-data.service';
|
||||
import { AuthorizationDataService } from '../../../core/data/feature-authorization/authorization-data.service';
|
||||
import { FeatureID } from '../../../core/data/feature-authorization/feature-id';
|
||||
import { RemoteData } from '../../../core/data/remote-data';
|
||||
import { ConfigurationProperty } from '../../../core/shared/configuration-property.model';
|
||||
import { getFirstCompletedRemoteData } from '../../../core/shared/operators';
|
||||
import { LinkMenuItemModel } from '../menu-item/models/link.model';
|
||||
import { TextMenuItemModel } from '../menu-item/models/text.model';
|
||||
import { MenuItemType } from '../menu-item-type.model';
|
||||
import { PartialMenuSection } from '../menu-provider.model';
|
||||
import { AbstractExpandableMenuProvider } from './helper-providers/expandable-menu-provider';
|
||||
|
||||
/**
|
||||
* Menu provider to create the "Reports" menu (and subsections) in the admin sidebar
|
||||
*/
|
||||
@Injectable()
|
||||
export class CreateReportMenuProvider extends AbstractExpandableMenuProvider {
|
||||
constructor(
|
||||
protected authorizationService: AuthorizationDataService,
|
||||
protected configurationDataService: ConfigurationDataService,
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
getSubSections(): Observable<PartialMenuSection[]> {
|
||||
return observableCombineLatest([
|
||||
this.configurationDataService.findByPropertyName('contentreport.enable').pipe(
|
||||
getFirstCompletedRemoteData(),
|
||||
map((res: RemoteData<ConfigurationProperty>) => res.hasSucceeded && res.payload && res.payload.values[0] === 'true'),
|
||||
),
|
||||
this.authorizationService.isAuthorized(FeatureID.AdministratorOf),
|
||||
]).pipe(
|
||||
map(([reportEnabled, isSiteAdmin]: [boolean, boolean]) => {
|
||||
return [
|
||||
/* Collections Report */
|
||||
{
|
||||
visible: isSiteAdmin && reportEnabled,
|
||||
model: {
|
||||
type: MenuItemType.LINK,
|
||||
text: 'menu.section.reports.collections',
|
||||
link: '/admin/reports/collections',
|
||||
} as LinkMenuItemModel,
|
||||
icon: 'user-check',
|
||||
},
|
||||
/* Queries Report */
|
||||
{
|
||||
visible: isSiteAdmin && reportEnabled,
|
||||
model: {
|
||||
type: MenuItemType.LINK,
|
||||
text: 'menu.section.reports.queries',
|
||||
link: '/admin/reports/queries',
|
||||
} as LinkMenuItemModel,
|
||||
icon: 'user-check',
|
||||
},
|
||||
];
|
||||
}));
|
||||
}
|
||||
|
||||
getTopSection(): Observable<PartialMenuSection> {
|
||||
return observableCombineLatest([
|
||||
this.configurationDataService.findByPropertyName('contentreport.enable').pipe(
|
||||
getFirstCompletedRemoteData(),
|
||||
map((res: RemoteData<ConfigurationProperty>) => res.hasSucceeded && res.payload && res.payload.values[0] === 'true'),
|
||||
),
|
||||
this.authorizationService.isAuthorized(FeatureID.AdministratorOf),
|
||||
]).pipe(
|
||||
map(([reportEnabled, isSiteAdmin]: [boolean, boolean]) => {
|
||||
return {
|
||||
visible: isSiteAdmin && reportEnabled,
|
||||
model: {
|
||||
type: MenuItemType.TEXT,
|
||||
text: 'menu.section.reports',
|
||||
} as TextMenuItemModel,
|
||||
icon: 'file-alt',
|
||||
};
|
||||
}));
|
||||
}
|
||||
}
|
58
src/app/shared/menu/providers/curation.menu.spec.ts
Normal file
58
src/app/shared/menu/providers/curation.menu.spec.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
/**
|
||||
* The contents of this file are subject to the license and copyright
|
||||
* detailed in the LICENSE and NOTICE files at the root of the source
|
||||
* tree and available online at
|
||||
*
|
||||
* http://www.dspace.org/license/
|
||||
*/
|
||||
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
import { of as observableOf } from 'rxjs';
|
||||
|
||||
import { AuthorizationDataService } from '../../../core/data/feature-authorization/authorization-data.service';
|
||||
import { AuthorizationDataServiceStub } from '../../testing/authorization-service.stub';
|
||||
import { MenuItemType } from '../menu-item-type.model';
|
||||
import { PartialMenuSection } from '../menu-provider.model';
|
||||
import { CurationMenuProvider } from './curation.menu';
|
||||
|
||||
describe('CurationMenuProvider', () => {
|
||||
const expectedSections: PartialMenuSection[] = [
|
||||
{
|
||||
visible: true,
|
||||
model: {
|
||||
type: MenuItemType.LINK,
|
||||
text: 'menu.section.curation_task',
|
||||
link: 'admin/curation-tasks',
|
||||
},
|
||||
icon: 'filter',
|
||||
},
|
||||
];
|
||||
|
||||
let provider: CurationMenuProvider;
|
||||
let authorizationServiceStub = new AuthorizationDataServiceStub();
|
||||
|
||||
beforeEach(() => {
|
||||
spyOn(authorizationServiceStub, 'isAuthorized').and.returnValue(
|
||||
observableOf(true),
|
||||
);
|
||||
|
||||
TestBed.configureTestingModule({
|
||||
providers: [
|
||||
CurationMenuProvider,
|
||||
{ provide: AuthorizationDataService, useValue: authorizationServiceStub },
|
||||
],
|
||||
});
|
||||
provider = TestBed.inject(CurationMenuProvider);
|
||||
});
|
||||
|
||||
it('should be created', () => {
|
||||
expect(provider).toBeTruthy();
|
||||
});
|
||||
|
||||
it('getSections should return expected menu sections', (done) => {
|
||||
provider.getSections().subscribe((sections) => {
|
||||
expect(sections).toEqual(expectedSections);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
55
src/app/shared/menu/providers/curation.menu.ts
Normal file
55
src/app/shared/menu/providers/curation.menu.ts
Normal file
@@ -0,0 +1,55 @@
|
||||
/**
|
||||
* The contents of this file are subject to the license and copyright
|
||||
* detailed in the LICENSE and NOTICE files at the root of the source
|
||||
* tree and available online at
|
||||
*
|
||||
* http://www.dspace.org/license/
|
||||
*/
|
||||
|
||||
import { Injectable } from '@angular/core';
|
||||
import {
|
||||
combineLatest,
|
||||
map,
|
||||
Observable,
|
||||
} from 'rxjs';
|
||||
|
||||
import { AuthorizationDataService } from '../../../core/data/feature-authorization/authorization-data.service';
|
||||
import { FeatureID } from '../../../core/data/feature-authorization/feature-id';
|
||||
import { LinkMenuItemModel } from '../menu-item/models/link.model';
|
||||
import { MenuItemType } from '../menu-item-type.model';
|
||||
import {
|
||||
AbstractMenuProvider,
|
||||
PartialMenuSection,
|
||||
} from '../menu-provider.model';
|
||||
|
||||
/**
|
||||
* Menu provider to create the "Curation Task" menu in the admin sidebar
|
||||
*/
|
||||
@Injectable()
|
||||
export class CurationMenuProvider extends AbstractMenuProvider {
|
||||
constructor(
|
||||
protected authorizationService: AuthorizationDataService,
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
public getSections(): Observable<PartialMenuSection[]> {
|
||||
return combineLatest([
|
||||
this.authorizationService.isAuthorized(FeatureID.AdministratorOf),
|
||||
]).pipe(
|
||||
map(([isSiteAdmin]) => {
|
||||
return [
|
||||
{
|
||||
visible: isSiteAdmin,
|
||||
model: {
|
||||
type: MenuItemType.LINK,
|
||||
text: 'menu.section.curation_task',
|
||||
link: 'admin/curation-tasks',
|
||||
} as LinkMenuItemModel,
|
||||
icon: 'filter',
|
||||
},
|
||||
] as PartialMenuSection[];
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
66
src/app/shared/menu/providers/dso-edit.menu.spec.ts
Normal file
66
src/app/shared/menu/providers/dso-edit.menu.spec.ts
Normal file
@@ -0,0 +1,66 @@
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
import { of as observableOf } from 'rxjs';
|
||||
|
||||
import { AuthorizationDataService } from '../../../core/data/feature-authorization/authorization-data.service';
|
||||
import { Collection } from '../../../core/shared/collection.model';
|
||||
import { COLLECTION } from '../../../core/shared/collection.resource-type';
|
||||
import { URLCombiner } from '../../../core/url-combiner/url-combiner';
|
||||
import { MenuItemType } from '../menu-item-type.model';
|
||||
import { PartialMenuSection } from '../menu-provider.model';
|
||||
import { DSpaceObjectEditMenuProvider } from './dso-edit.menu';
|
||||
|
||||
describe('DSpaceObjectEditMenuProvider', () => {
|
||||
|
||||
const expectedSections: PartialMenuSection[] = [
|
||||
{
|
||||
visible: true,
|
||||
model: {
|
||||
type: MenuItemType.LINK,
|
||||
text: 'collection.page.edit',
|
||||
link: new URLCombiner('/collections/test-uuid', 'edit', 'metadata').toString(),
|
||||
},
|
||||
icon: 'pencil-alt',
|
||||
},
|
||||
];
|
||||
|
||||
let provider: DSpaceObjectEditMenuProvider;
|
||||
|
||||
const dso: Collection = Object.assign(new Collection(), {
|
||||
type: COLLECTION.value,
|
||||
uuid: 'test-uuid',
|
||||
_links: { self: { href: 'self-link' } },
|
||||
});
|
||||
|
||||
|
||||
let authorizationService;
|
||||
|
||||
beforeEach(() => {
|
||||
|
||||
authorizationService = jasmine.createSpyObj('authorizationService', {
|
||||
'isAuthorized': observableOf(true),
|
||||
});
|
||||
|
||||
TestBed.configureTestingModule({
|
||||
providers: [
|
||||
DSpaceObjectEditMenuProvider,
|
||||
{ provide: AuthorizationDataService, useValue: authorizationService },
|
||||
],
|
||||
});
|
||||
provider = TestBed.inject(DSpaceObjectEditMenuProvider);
|
||||
});
|
||||
|
||||
it('should be created', () => {
|
||||
expect(provider).toBeTruthy();
|
||||
});
|
||||
|
||||
describe('getSectionsForContext', () => {
|
||||
it('should return the expected sections', (done) => {
|
||||
provider.getSectionsForContext(dso).subscribe((sections) => {
|
||||
expect(sections).toEqual(expectedSections);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
});
|
55
src/app/shared/menu/providers/dso-edit.menu.ts
Normal file
55
src/app/shared/menu/providers/dso-edit.menu.ts
Normal file
@@ -0,0 +1,55 @@
|
||||
/**
|
||||
* The contents of this file are subject to the license and copyright
|
||||
* detailed in the LICENSE and NOTICE files at the root of the source
|
||||
* tree and available online at
|
||||
*
|
||||
* http://www.dspace.org/license/
|
||||
*/
|
||||
import { Injectable } from '@angular/core';
|
||||
import {
|
||||
combineLatest,
|
||||
Observable,
|
||||
} from 'rxjs';
|
||||
import { map } from 'rxjs/operators';
|
||||
|
||||
import { getDSORoute } from '../../../app-routing-paths';
|
||||
import { AuthorizationDataService } from '../../../core/data/feature-authorization/authorization-data.service';
|
||||
import { FeatureID } from '../../../core/data/feature-authorization/feature-id';
|
||||
import { DSpaceObject } from '../../../core/shared/dspace-object.model';
|
||||
import { URLCombiner } from '../../../core/url-combiner/url-combiner';
|
||||
import { LinkMenuItemModel } from '../menu-item/models/link.model';
|
||||
import { MenuItemType } from '../menu-item-type.model';
|
||||
import { PartialMenuSection } from '../menu-provider.model';
|
||||
import { DSpaceObjectPageMenuProvider } from './helper-providers/dso.menu';
|
||||
|
||||
/**
|
||||
* Menu provider to create the "Edit" option in the DSO edit menu
|
||||
*/
|
||||
@Injectable()
|
||||
export class DSpaceObjectEditMenuProvider extends DSpaceObjectPageMenuProvider {
|
||||
constructor(
|
||||
protected authorizationDataService: AuthorizationDataService,
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
public getSectionsForContext(dso: DSpaceObject): Observable<PartialMenuSection[]> {
|
||||
return combineLatest([
|
||||
this.authorizationDataService.isAuthorized(FeatureID.CanEditMetadata, dso.self),
|
||||
]).pipe(
|
||||
map(([canEditItem]) => {
|
||||
return [
|
||||
{
|
||||
visible: canEditItem,
|
||||
model: {
|
||||
type: MenuItemType.LINK,
|
||||
text: this.getDsoType(dso) + '.page.edit',
|
||||
link: new URLCombiner(getDSORoute(dso), 'edit', 'metadata').toString(),
|
||||
} as LinkMenuItemModel,
|
||||
icon: 'pencil-alt',
|
||||
},
|
||||
] as PartialMenuSection[];
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
54
src/app/shared/menu/providers/dso-option.menu.spec.ts
Normal file
54
src/app/shared/menu/providers/dso-option.menu.spec.ts
Normal file
@@ -0,0 +1,54 @@
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
|
||||
import { Collection } from '../../../core/shared/collection.model';
|
||||
import { COLLECTION } from '../../../core/shared/collection.resource-type';
|
||||
import { MenuItemType } from '../menu-item-type.model';
|
||||
import { PartialMenuSection } from '../menu-provider.model';
|
||||
import { DsoOptionMenuProvider } from './dso-option.menu';
|
||||
|
||||
describe('DsoOptionMenuProvider', () => {
|
||||
|
||||
const expectedSections: PartialMenuSection[] = [
|
||||
{
|
||||
visible: true,
|
||||
model: {
|
||||
type: MenuItemType.TEXT,
|
||||
text: 'collection.page.options',
|
||||
},
|
||||
icon: 'ellipsis-vertical',
|
||||
},
|
||||
];
|
||||
|
||||
let provider: DsoOptionMenuProvider;
|
||||
|
||||
const dso: Collection = Object.assign(new Collection(), {
|
||||
type: COLLECTION.value,
|
||||
uuid: 'test-uuid',
|
||||
_links: { self: { href: 'self-link' } },
|
||||
});
|
||||
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
providers: [
|
||||
DsoOptionMenuProvider,
|
||||
],
|
||||
});
|
||||
provider = TestBed.inject(DsoOptionMenuProvider);
|
||||
});
|
||||
|
||||
it('should be created', () => {
|
||||
expect(provider).toBeTruthy();
|
||||
});
|
||||
|
||||
describe('getSectionsForContext', () => {
|
||||
it('should return the expected sections', (done) => {
|
||||
provider.getSectionsForContext(dso).subscribe((sections) => {
|
||||
expect(sections).toEqual(expectedSections);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
});
|
41
src/app/shared/menu/providers/dso-option.menu.ts
Normal file
41
src/app/shared/menu/providers/dso-option.menu.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
/**
|
||||
* The contents of this file are subject to the license and copyright
|
||||
* detailed in the LICENSE and NOTICE files at the root of the source
|
||||
* tree and available online at
|
||||
*
|
||||
* http://www.dspace.org/license/
|
||||
*/
|
||||
|
||||
import { Injectable } from '@angular/core';
|
||||
import {
|
||||
Observable,
|
||||
of,
|
||||
} from 'rxjs';
|
||||
import { DSpaceObject } from 'src/app/core/shared/dspace-object.model';
|
||||
|
||||
import { MenuItemType } from '../menu-item-type.model';
|
||||
import { PartialMenuSection } from '../menu-provider.model';
|
||||
import { DSpaceObjectPageMenuProvider } from './helper-providers/dso.menu';
|
||||
|
||||
/**
|
||||
* Menu provider to create the "Options" menu wrapper on the DSO pages.
|
||||
* This section will be rendered as a button on the DSO pages if sub providers have been added.
|
||||
*/
|
||||
@Injectable()
|
||||
export class DsoOptionMenuProvider extends DSpaceObjectPageMenuProvider {
|
||||
|
||||
alwaysRenderExpandable = true;
|
||||
|
||||
getSectionsForContext(dso: DSpaceObject): Observable<PartialMenuSection[]> {
|
||||
return of([
|
||||
{
|
||||
visible: true,
|
||||
model: {
|
||||
type: MenuItemType.TEXT,
|
||||
text: this.getDsoType(dso) + '.page.options',
|
||||
},
|
||||
icon: 'ellipsis-vertical',
|
||||
},
|
||||
] as PartialMenuSection[]);
|
||||
}
|
||||
}
|
96
src/app/shared/menu/providers/edit.menu.spec.ts
Normal file
96
src/app/shared/menu/providers/edit.menu.spec.ts
Normal file
@@ -0,0 +1,96 @@
|
||||
/**
|
||||
* The contents of this file are subject to the license and copyright
|
||||
* detailed in the LICENSE and NOTICE files at the root of the source
|
||||
* tree and available online at
|
||||
*
|
||||
* http://www.dspace.org/license/
|
||||
*/
|
||||
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
import { of as observableOf } from 'rxjs';
|
||||
|
||||
import { AuthorizationDataService } from '../../../core/data/feature-authorization/authorization-data.service';
|
||||
import { FeatureID } from '../../../core/data/feature-authorization/feature-id';
|
||||
import { AuthorizationDataServiceStub } from '../../testing/authorization-service.stub';
|
||||
import { MenuItemType } from '../menu-item-type.model';
|
||||
import { PartialMenuSection } from '../menu-provider.model';
|
||||
import { EditMenuProvider } from './edit.menu';
|
||||
|
||||
describe('EditMenuProvider', () => {
|
||||
|
||||
const expectedTopSection: PartialMenuSection = {
|
||||
accessibilityHandle: 'edit',
|
||||
visible: true,
|
||||
model: {
|
||||
type: MenuItemType.TEXT,
|
||||
text: 'menu.section.edit',
|
||||
},
|
||||
icon: 'pencil',
|
||||
};
|
||||
|
||||
const expectedSubSections: PartialMenuSection[] = [
|
||||
{
|
||||
visible: true,
|
||||
model: {
|
||||
type: MenuItemType.ONCLICK,
|
||||
text: 'menu.section.edit_community',
|
||||
function: jasmine.any(Function) as any,
|
||||
},
|
||||
},
|
||||
{
|
||||
visible: false,
|
||||
model: {
|
||||
type: MenuItemType.ONCLICK,
|
||||
text: 'menu.section.edit_collection',
|
||||
function: jasmine.any(Function) as any,
|
||||
},
|
||||
},
|
||||
{
|
||||
visible: true,
|
||||
model: {
|
||||
type: MenuItemType.ONCLICK,
|
||||
text: 'menu.section.edit_item',
|
||||
function: jasmine.any(Function) as any,
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
let provider: EditMenuProvider;
|
||||
let authorizationServiceStub = new AuthorizationDataServiceStub();
|
||||
|
||||
beforeEach(() => {
|
||||
spyOn(authorizationServiceStub, 'isAuthorized').and.callFake((id: FeatureID) => {
|
||||
if (id === FeatureID.IsCollectionAdmin) {
|
||||
return observableOf(false);
|
||||
} else {
|
||||
return observableOf(true);
|
||||
}
|
||||
});
|
||||
|
||||
TestBed.configureTestingModule({
|
||||
providers: [
|
||||
EditMenuProvider,
|
||||
{ provide: AuthorizationDataService, useValue: authorizationServiceStub },
|
||||
],
|
||||
});
|
||||
provider = TestBed.inject(EditMenuProvider);
|
||||
});
|
||||
|
||||
it('should be created', () => {
|
||||
expect(provider).toBeTruthy();
|
||||
});
|
||||
|
||||
it('getTopSection should return expected menu section', (done) => {
|
||||
provider.getTopSection().subscribe((section) => {
|
||||
expect(section).toEqual(expectedTopSection);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('getSubSections should return expected menu sections', (done) => {
|
||||
provider.getSubSections().subscribe((sections) => {
|
||||
expect(sections).toEqual(expectedSubSections);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
95
src/app/shared/menu/providers/edit.menu.ts
Normal file
95
src/app/shared/menu/providers/edit.menu.ts
Normal file
@@ -0,0 +1,95 @@
|
||||
/**
|
||||
* The contents of this file are subject to the license and copyright
|
||||
* detailed in the LICENSE and NOTICE files at the root of the source
|
||||
* tree and available online at
|
||||
*
|
||||
* http://www.dspace.org/license/
|
||||
*/
|
||||
|
||||
import { Injectable } from '@angular/core';
|
||||
import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
|
||||
import {
|
||||
combineLatest,
|
||||
map,
|
||||
Observable,
|
||||
of as observableOf,
|
||||
} from 'rxjs';
|
||||
|
||||
import { AuthorizationDataService } from '../../../core/data/feature-authorization/authorization-data.service';
|
||||
import { FeatureID } from '../../../core/data/feature-authorization/feature-id';
|
||||
import { ThemedEditCollectionSelectorComponent } from '../../dso-selector/modal-wrappers/edit-collection-selector/themed-edit-collection-selector.component';
|
||||
import { ThemedEditCommunitySelectorComponent } from '../../dso-selector/modal-wrappers/edit-community-selector/themed-edit-community-selector.component';
|
||||
import { ThemedEditItemSelectorComponent } from '../../dso-selector/modal-wrappers/edit-item-selector/themed-edit-item-selector.component';
|
||||
import { MenuItemType } from '../menu-item-type.model';
|
||||
import { PartialMenuSection } from '../menu-provider.model';
|
||||
import { AbstractExpandableMenuProvider } from './helper-providers/expandable-menu-provider';
|
||||
|
||||
/**
|
||||
* Menu provider to create the "Edit" menu (and subsections) in the admin sidebar
|
||||
*/
|
||||
@Injectable()
|
||||
export class EditMenuProvider extends AbstractExpandableMenuProvider {
|
||||
constructor(
|
||||
protected authorizationService: AuthorizationDataService,
|
||||
protected modalService: NgbModal,
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
public getTopSection(): Observable<PartialMenuSection> {
|
||||
return observableOf(
|
||||
{
|
||||
accessibilityHandle: 'edit',
|
||||
model: {
|
||||
type: MenuItemType.TEXT,
|
||||
text: 'menu.section.edit',
|
||||
},
|
||||
icon: 'pencil',
|
||||
visible: true,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
public getSubSections(): Observable<PartialMenuSection[]> {
|
||||
return combineLatest([
|
||||
this.authorizationService.isAuthorized(FeatureID.IsCollectionAdmin),
|
||||
this.authorizationService.isAuthorized(FeatureID.IsCommunityAdmin),
|
||||
this.authorizationService.isAuthorized(FeatureID.CanEditItem),
|
||||
]).pipe(
|
||||
map(([isCollectionAdmin, isCommunityAdmin, canEditItem]: [boolean, boolean, boolean]) => {
|
||||
return [
|
||||
{
|
||||
visible: isCommunityAdmin,
|
||||
model: {
|
||||
type: MenuItemType.ONCLICK,
|
||||
text: 'menu.section.edit_community',
|
||||
function: () => {
|
||||
this.modalService.open(ThemedEditCommunitySelectorComponent);
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
visible: isCollectionAdmin,
|
||||
model: {
|
||||
type: MenuItemType.ONCLICK,
|
||||
text: 'menu.section.edit_collection',
|
||||
function: () => {
|
||||
this.modalService.open(ThemedEditCollectionSelectorComponent);
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
visible: canEditItem,
|
||||
model: {
|
||||
type: MenuItemType.ONCLICK,
|
||||
text: 'menu.section.edit_item',
|
||||
function: () => {
|
||||
this.modalService.open(ThemedEditItemSelectorComponent);
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
85
src/app/shared/menu/providers/export.menu.spec.ts
Normal file
85
src/app/shared/menu/providers/export.menu.spec.ts
Normal file
@@ -0,0 +1,85 @@
|
||||
/**
|
||||
* The contents of this file are subject to the license and copyright
|
||||
* detailed in the LICENSE and NOTICE files at the root of the source
|
||||
* tree and available online at
|
||||
*
|
||||
* http://www.dspace.org/license/
|
||||
*/
|
||||
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
import { of as observableOf } from 'rxjs';
|
||||
|
||||
import { AuthorizationDataService } from '../../../core/data/feature-authorization/authorization-data.service';
|
||||
import { ScriptDataService } from '../../../core/data/processes/script-data.service';
|
||||
import { AuthorizationDataServiceStub } from '../../testing/authorization-service.stub';
|
||||
import { ScriptServiceStub } from '../../testing/script-service.stub';
|
||||
import { MenuItemType } from '../menu-item-type.model';
|
||||
import { PartialMenuSection } from '../menu-provider.model';
|
||||
import { ExportMenuProvider } from './export.menu';
|
||||
|
||||
describe('ExportMenuProvider', () => {
|
||||
const expectedTopSection: PartialMenuSection = {
|
||||
accessibilityHandle: 'export',
|
||||
visible: true,
|
||||
model: {
|
||||
type: MenuItemType.TEXT,
|
||||
text: 'menu.section.export',
|
||||
},
|
||||
icon: 'file-export',
|
||||
};
|
||||
|
||||
const expectedSubSections: PartialMenuSection[] = [
|
||||
{
|
||||
visible: true,
|
||||
model: {
|
||||
type: MenuItemType.ONCLICK,
|
||||
text: 'menu.section.export_metadata',
|
||||
function: jasmine.any(Function) as any,
|
||||
},
|
||||
},
|
||||
{
|
||||
visible: true,
|
||||
model: {
|
||||
type: MenuItemType.ONCLICK,
|
||||
text: 'menu.section.export_batch',
|
||||
function: jasmine.any(Function) as any,
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
let provider: ExportMenuProvider;
|
||||
let authorizationServiceStub = new AuthorizationDataServiceStub();
|
||||
|
||||
beforeEach(() => {
|
||||
spyOn(authorizationServiceStub, 'isAuthorized').and.returnValue(
|
||||
observableOf(true),
|
||||
);
|
||||
|
||||
TestBed.configureTestingModule({
|
||||
providers: [
|
||||
ExportMenuProvider,
|
||||
{ provide: AuthorizationDataService, useValue: authorizationServiceStub },
|
||||
{ provide: ScriptDataService, useClass: ScriptServiceStub },
|
||||
],
|
||||
});
|
||||
provider = TestBed.inject(ExportMenuProvider);
|
||||
});
|
||||
|
||||
it('should be created', () => {
|
||||
expect(provider).toBeTruthy();
|
||||
});
|
||||
|
||||
it('getTopSection should return expected menu section', (done) => {
|
||||
provider.getTopSection().subscribe((section) => {
|
||||
expect(section).toEqual(expectedTopSection);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('getSubSections should return expected menu sections', (done) => {
|
||||
provider.getSubSections().subscribe((sections) => {
|
||||
expect(sections).toEqual(expectedSubSections);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
88
src/app/shared/menu/providers/export.menu.ts
Normal file
88
src/app/shared/menu/providers/export.menu.ts
Normal file
@@ -0,0 +1,88 @@
|
||||
/**
|
||||
* The contents of this file are subject to the license and copyright
|
||||
* detailed in the LICENSE and NOTICE files at the root of the source
|
||||
* tree and available online at
|
||||
*
|
||||
* http://www.dspace.org/license/
|
||||
*/
|
||||
|
||||
import { Injectable } from '@angular/core';
|
||||
import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
|
||||
import {
|
||||
combineLatest as observableCombineLatest,
|
||||
map,
|
||||
Observable,
|
||||
of as observableOf,
|
||||
} from 'rxjs';
|
||||
|
||||
import { AuthorizationDataService } from '../../../core/data/feature-authorization/authorization-data.service';
|
||||
import { FeatureID } from '../../../core/data/feature-authorization/feature-id';
|
||||
import {
|
||||
METADATA_EXPORT_SCRIPT_NAME,
|
||||
ScriptDataService,
|
||||
} from '../../../core/data/processes/script-data.service';
|
||||
import { ExportBatchSelectorComponent } from '../../dso-selector/modal-wrappers/export-batch-selector/export-batch-selector.component';
|
||||
import { ExportMetadataSelectorComponent } from '../../dso-selector/modal-wrappers/export-metadata-selector/export-metadata-selector.component';
|
||||
import { MenuItemType } from '../menu-item-type.model';
|
||||
import { PartialMenuSection } from '../menu-provider.model';
|
||||
import { AbstractExpandableMenuProvider } from './helper-providers/expandable-menu-provider';
|
||||
|
||||
/**
|
||||
* Menu provider to create the "Export" menu (and subsections) in the admin sidebar
|
||||
*/
|
||||
@Injectable()
|
||||
export class ExportMenuProvider extends AbstractExpandableMenuProvider {
|
||||
constructor(
|
||||
protected authorizationService: AuthorizationDataService,
|
||||
protected scriptDataService: ScriptDataService,
|
||||
protected modalService: NgbModal,
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
public getTopSection(): Observable<PartialMenuSection> {
|
||||
return observableOf(
|
||||
{
|
||||
accessibilityHandle: 'export',
|
||||
model: {
|
||||
type: MenuItemType.TEXT,
|
||||
text: 'menu.section.export',
|
||||
},
|
||||
icon: 'file-export',
|
||||
visible: true,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
public getSubSections(): Observable<PartialMenuSection[]> {
|
||||
return observableCombineLatest([
|
||||
this.authorizationService.isAuthorized(FeatureID.AdministratorOf),
|
||||
this.scriptDataService.scriptWithNameExistsAndCanExecute(METADATA_EXPORT_SCRIPT_NAME),
|
||||
]).pipe(
|
||||
map(([authorized, metadataExportScriptExists]: [boolean, boolean]) => {
|
||||
return [
|
||||
{
|
||||
visible: authorized && metadataExportScriptExists,
|
||||
model: {
|
||||
type: MenuItemType.ONCLICK,
|
||||
text: 'menu.section.export_metadata',
|
||||
function: () => {
|
||||
this.modalService.open(ExportMetadataSelectorComponent);
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
visible: authorized && metadataExportScriptExists,
|
||||
model: {
|
||||
type: MenuItemType.ONCLICK,
|
||||
text: 'menu.section.export_batch',
|
||||
function: () => {
|
||||
this.modalService.open(ExportBatchSelectorComponent);
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
58
src/app/shared/menu/providers/health.menu.spec.ts
Normal file
58
src/app/shared/menu/providers/health.menu.spec.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
/**
|
||||
* The contents of this file are subject to the license and copyright
|
||||
* detailed in the LICENSE and NOTICE files at the root of the source
|
||||
* tree and available online at
|
||||
*
|
||||
* http://www.dspace.org/license/
|
||||
*/
|
||||
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
import { of as observableOf } from 'rxjs';
|
||||
|
||||
import { AuthorizationDataService } from '../../../core/data/feature-authorization/authorization-data.service';
|
||||
import { AuthorizationDataServiceStub } from '../../testing/authorization-service.stub';
|
||||
import { MenuItemType } from '../menu-item-type.model';
|
||||
import { PartialMenuSection } from '../menu-provider.model';
|
||||
import { HealthMenuProvider } from './health.menu';
|
||||
|
||||
describe('HealthMenuProvider', () => {
|
||||
const expectedSections: PartialMenuSection[] = [
|
||||
{
|
||||
visible: true,
|
||||
model: {
|
||||
type: MenuItemType.LINK,
|
||||
text: 'menu.section.health',
|
||||
link: '/health',
|
||||
},
|
||||
icon: 'heartbeat',
|
||||
},
|
||||
];
|
||||
|
||||
let provider: HealthMenuProvider;
|
||||
let authorizationServiceStub = new AuthorizationDataServiceStub();
|
||||
|
||||
beforeEach(() => {
|
||||
spyOn(authorizationServiceStub, 'isAuthorized').and.returnValue(
|
||||
observableOf(true),
|
||||
);
|
||||
|
||||
TestBed.configureTestingModule({
|
||||
providers: [
|
||||
HealthMenuProvider,
|
||||
{ provide: AuthorizationDataService, useValue: authorizationServiceStub },
|
||||
],
|
||||
});
|
||||
provider = TestBed.inject(HealthMenuProvider);
|
||||
});
|
||||
|
||||
it('should be created', () => {
|
||||
expect(provider).toBeTruthy();
|
||||
});
|
||||
|
||||
it('getSections should return expected menu sections', (done) => {
|
||||
provider.getSections().subscribe((sections) => {
|
||||
expect(sections).toEqual(expectedSections);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
54
src/app/shared/menu/providers/health.menu.ts
Normal file
54
src/app/shared/menu/providers/health.menu.ts
Normal file
@@ -0,0 +1,54 @@
|
||||
/**
|
||||
* The contents of this file are subject to the license and copyright
|
||||
* detailed in the LICENSE and NOTICE files at the root of the source
|
||||
* tree and available online at
|
||||
*
|
||||
* http://www.dspace.org/license/
|
||||
*/
|
||||
|
||||
import { Injectable } from '@angular/core';
|
||||
import {
|
||||
combineLatest,
|
||||
map,
|
||||
Observable,
|
||||
} from 'rxjs';
|
||||
|
||||
import { AuthorizationDataService } from '../../../core/data/feature-authorization/authorization-data.service';
|
||||
import { FeatureID } from '../../../core/data/feature-authorization/feature-id';
|
||||
import { MenuItemType } from '../menu-item-type.model';
|
||||
import {
|
||||
AbstractMenuProvider,
|
||||
PartialMenuSection,
|
||||
} from '../menu-provider.model';
|
||||
|
||||
/**
|
||||
* Menu provider to create the "Health" menu in the admin sidebar
|
||||
*/
|
||||
@Injectable()
|
||||
export class HealthMenuProvider extends AbstractMenuProvider {
|
||||
constructor(
|
||||
protected authorizationService: AuthorizationDataService,
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
public getSections(): Observable<PartialMenuSection[]> {
|
||||
return combineLatest([
|
||||
this.authorizationService.isAuthorized(FeatureID.AdministratorOf),
|
||||
]).pipe(
|
||||
map(([isSiteAdmin]) => {
|
||||
return [
|
||||
{
|
||||
visible: isSiteAdmin,
|
||||
model: {
|
||||
type: MenuItemType.LINK,
|
||||
text: 'menu.section.health',
|
||||
link: '/health',
|
||||
},
|
||||
icon: 'heartbeat',
|
||||
},
|
||||
] as PartialMenuSection[];
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
139
src/app/shared/menu/providers/helper-providers/dso.menu.spec.ts
Normal file
139
src/app/shared/menu/providers/helper-providers/dso.menu.spec.ts
Normal file
@@ -0,0 +1,139 @@
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
|
||||
import { Collection } from '../../../../core/shared/collection.model';
|
||||
import { COLLECTION } from '../../../../core/shared/collection.resource-type';
|
||||
import { Item } from '../../../../core/shared/item.model';
|
||||
import { ITEM } from '../../../../core/shared/item.resource-type';
|
||||
import { createSuccessfulRemoteDataObject } from '../../../remote-data.utils';
|
||||
import { DSpaceObjectPageMenuProvider } from './dso.menu';
|
||||
|
||||
describe('DSpaceObjectPageMenuProvider', () => {
|
||||
|
||||
let provider: DSpaceObjectPageMenuProvider;
|
||||
|
||||
const item: Item = Object.assign(new Item(), {
|
||||
uuid: 'test-item-uuid',
|
||||
type: ITEM.value,
|
||||
_links: { self: { href: 'self-link' } },
|
||||
metadata: {
|
||||
'dc.title': [{
|
||||
'value': 'Untyped Item',
|
||||
}],
|
||||
},
|
||||
});
|
||||
|
||||
const item2: Item = Object.assign(new Item(), {
|
||||
uuid: 'test-item2-uuid',
|
||||
type: ITEM.value,
|
||||
_links: { self: { href: 'self-link' } },
|
||||
metadata: {
|
||||
'dc.title': [{
|
||||
'value': 'Untyped Item 2',
|
||||
}],
|
||||
},
|
||||
});
|
||||
|
||||
const person: Item = Object.assign(new Item(), {
|
||||
uuid: 'test-uuid',
|
||||
type: ITEM.value,
|
||||
_links: { self: { href: 'self-link' } },
|
||||
metadata: {
|
||||
'dc.title': [{
|
||||
'value': 'Person Entity',
|
||||
}],
|
||||
'dspace.entity.type': [{
|
||||
'value': 'Person',
|
||||
}],
|
||||
},
|
||||
});
|
||||
|
||||
const collection: Collection = Object.assign(new Collection(), {
|
||||
uuid: 'test-collection-uuid',
|
||||
type: COLLECTION.value,
|
||||
_links: { self: { href: 'self-link' } },
|
||||
metadata: {
|
||||
'dc.title': [{
|
||||
'value': 'Collection',
|
||||
}],
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
beforeEach(() => {
|
||||
|
||||
TestBed.configureTestingModule({
|
||||
providers: [
|
||||
DSpaceObjectPageMenuProvider,
|
||||
],
|
||||
});
|
||||
provider = TestBed.inject(DSpaceObjectPageMenuProvider);
|
||||
});
|
||||
|
||||
it('should be created', () => {
|
||||
expect(provider).toBeTruthy();
|
||||
});
|
||||
|
||||
describe('getRouteContext', () => {
|
||||
it('should get the dso from the route', (done) => {
|
||||
const route = { data: { dso: createSuccessfulRemoteDataObject(item) } } as any;
|
||||
|
||||
provider.getRouteContext(route, undefined).subscribe((dso) => {
|
||||
expect(dso).toEqual(item);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('return the first parent DSO when no DSO is present on the current route', (done) => {
|
||||
const route = {
|
||||
data: {},
|
||||
parent: {
|
||||
data: {},
|
||||
parent: {
|
||||
data: { dso: createSuccessfulRemoteDataObject(item) },
|
||||
parent: { data: { dso: createSuccessfulRemoteDataObject(item2) } },
|
||||
},
|
||||
},
|
||||
} as any;
|
||||
|
||||
provider.getRouteContext(route, undefined).subscribe((dso) => {
|
||||
expect(dso).toEqual(item);
|
||||
done();
|
||||
});
|
||||
});
|
||||
it('should return undefined when no dso is found in the route', (done) => {
|
||||
const route = { data: {}, parent: { data: {}, parent: { data: {}, parent: { data: {} } } } } as any;
|
||||
|
||||
provider.getRouteContext(route, undefined).subscribe((dso) => {
|
||||
expect(dso).toBeUndefined();
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('getDsoType', () => {
|
||||
it('should return the object type for an untyped item', () => {
|
||||
const dsoType = (provider as any).getDsoType(item);
|
||||
expect(dsoType).toEqual('item');
|
||||
});
|
||||
it('should return the entity type for an entity item', () => {
|
||||
const dsoType = (provider as any).getDsoType(person);
|
||||
expect(dsoType).toEqual('person');
|
||||
});
|
||||
it('should return the object type for a colletion', () => {
|
||||
const dsoType = (provider as any).getDsoType(collection);
|
||||
expect(dsoType).toEqual('collection');
|
||||
});
|
||||
});
|
||||
|
||||
describe('isApplicable', () => {
|
||||
it('should return true when a dso is provided', () => {
|
||||
const isApplicable = (provider as any).isApplicable(collection);
|
||||
expect(isApplicable).toBeTrue();
|
||||
});
|
||||
it('should return false when no dso is provided', () => {
|
||||
const isApplicable = (provider as any).isApplicable(undefined);
|
||||
expect(isApplicable).toBeFalse();
|
||||
});
|
||||
});
|
||||
|
||||
});
|
63
src/app/shared/menu/providers/helper-providers/dso.menu.ts
Normal file
63
src/app/shared/menu/providers/helper-providers/dso.menu.ts
Normal file
@@ -0,0 +1,63 @@
|
||||
/**
|
||||
* The contents of this file are subject to the license and copyright
|
||||
* detailed in the LICENSE and NOTICE files at the root of the source
|
||||
* tree and available online at
|
||||
*
|
||||
* http://www.dspace.org/license/
|
||||
*/
|
||||
import {
|
||||
ActivatedRouteSnapshot,
|
||||
RouterStateSnapshot,
|
||||
} from '@angular/router';
|
||||
import {
|
||||
Observable,
|
||||
of,
|
||||
} from 'rxjs';
|
||||
|
||||
import { RemoteData } from '../../../../core/data/remote-data';
|
||||
import { DSpaceObject } from '../../../../core/shared/dspace-object.model';
|
||||
import {
|
||||
hasNoValue,
|
||||
hasValue,
|
||||
} from '../../../empty.util';
|
||||
import { AbstractRouteContextMenuProvider } from './route-context.menu';
|
||||
|
||||
/**
|
||||
* Helper provider for DSpace object page based menus
|
||||
*/
|
||||
export abstract class DSpaceObjectPageMenuProvider extends AbstractRouteContextMenuProvider<DSpaceObject> {
|
||||
|
||||
/**
|
||||
* Retrieve the dso from the current route data
|
||||
*/
|
||||
public getRouteContext(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<DSpaceObject | undefined> {
|
||||
let dsoRD: RemoteData<DSpaceObject> = route.data.dso;
|
||||
// Check if one of the parent routes has a DSO
|
||||
while (hasValue(route.parent) && hasNoValue(dsoRD)) {
|
||||
route = route.parent;
|
||||
dsoRD = route.data.dso;
|
||||
}
|
||||
|
||||
if (hasValue(dsoRD) && dsoRD.hasSucceeded && hasValue(dsoRD.payload)) {
|
||||
return of(dsoRD.payload);
|
||||
} else {
|
||||
return of(undefined);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the dso or entity type for an object to be used in section messages
|
||||
*/
|
||||
protected getDsoType(dso: DSpaceObject) {
|
||||
const renderType = dso.getRenderTypes()[0];
|
||||
if (typeof renderType === 'string' || renderType instanceof String) {
|
||||
return renderType.toLowerCase();
|
||||
} else {
|
||||
return dso.type.toString().toLowerCase();
|
||||
}
|
||||
}
|
||||
|
||||
protected isApplicable(dso: DSpaceObject): boolean {
|
||||
return hasValue(dso);
|
||||
}
|
||||
}
|
@@ -0,0 +1,116 @@
|
||||
/**
|
||||
* The contents of this file are subject to the license and copyright
|
||||
* detailed in the LICENSE and NOTICE files at the root of the source
|
||||
* tree and available online at
|
||||
*
|
||||
* http://www.dspace.org/license/
|
||||
*/
|
||||
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
import {
|
||||
Observable,
|
||||
of as observableOf,
|
||||
} from 'rxjs';
|
||||
|
||||
import { MenuID } from '../../menu-id.model';
|
||||
import { MenuItemType } from '../../menu-item-type.model';
|
||||
import { PartialMenuSection } from '../../menu-provider.model';
|
||||
import { AbstractExpandableMenuProvider } from './expandable-menu-provider';
|
||||
|
||||
describe('AbstractExpandableMenuProvider', () => {
|
||||
const topSection: PartialMenuSection = {
|
||||
visible: true,
|
||||
model: {
|
||||
type: MenuItemType.TEXT,
|
||||
text: 'top.section.test',
|
||||
},
|
||||
icon: 'file-import',
|
||||
};
|
||||
|
||||
const subSections: PartialMenuSection[] = [
|
||||
{
|
||||
visible: true,
|
||||
model: {
|
||||
type: MenuItemType.TEXT,
|
||||
text: 'sub.section.test.1',
|
||||
},
|
||||
},
|
||||
{
|
||||
visible: true,
|
||||
model: {
|
||||
type: MenuItemType.TEXT,
|
||||
text: 'sub.section.test.2',
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
class TestClass extends AbstractExpandableMenuProvider {
|
||||
getTopSection(): Observable<PartialMenuSection> {
|
||||
return observableOf(topSection);
|
||||
}
|
||||
|
||||
getSubSections(): Observable<PartialMenuSection[]> {
|
||||
return observableOf(subSections);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
const expectedSections: PartialMenuSection[] = [
|
||||
{
|
||||
visible: true,
|
||||
model: {
|
||||
type: MenuItemType.TEXT,
|
||||
text: 'sub.section.test.1',
|
||||
},
|
||||
id: `${MenuID.ADMIN}_1_0_0`,
|
||||
parentID: `${MenuID.ADMIN}_1_0`,
|
||||
alwaysRenderExpandable: false,
|
||||
},
|
||||
{
|
||||
visible: true,
|
||||
model: {
|
||||
type: MenuItemType.TEXT,
|
||||
text: 'sub.section.test.2',
|
||||
},
|
||||
id: `${MenuID.ADMIN}_1_0_1`,
|
||||
parentID: `${MenuID.ADMIN}_1_0`,
|
||||
alwaysRenderExpandable: false,
|
||||
},
|
||||
{
|
||||
visible: true,
|
||||
model: {
|
||||
type: MenuItemType.TEXT,
|
||||
text: 'top.section.test',
|
||||
},
|
||||
icon: 'file-import',
|
||||
id: `${MenuID.ADMIN}_1_0`,
|
||||
alwaysRenderExpandable: true,
|
||||
},
|
||||
];
|
||||
|
||||
let provider: AbstractExpandableMenuProvider;
|
||||
|
||||
beforeEach(() => {
|
||||
|
||||
TestBed.configureTestingModule({
|
||||
providers: [
|
||||
TestClass,
|
||||
],
|
||||
});
|
||||
provider = TestBed.inject(TestClass);
|
||||
provider.menuProviderId = `${MenuID.ADMIN}_1`;
|
||||
});
|
||||
|
||||
it('should be created', () => {
|
||||
expect(provider).toBeTruthy();
|
||||
});
|
||||
|
||||
it('getSections should return a combination of top and sub sections', (done) => {
|
||||
|
||||
provider.getSections().subscribe((section) => {
|
||||
expect(section).toEqual(expectedSections);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
});
|
@@ -0,0 +1,77 @@
|
||||
/**
|
||||
* The contents of this file are subject to the license and copyright
|
||||
* detailed in the LICENSE and NOTICE files at the root of the source
|
||||
* tree and available online at
|
||||
*
|
||||
* http://www.dspace.org/license/
|
||||
*/
|
||||
import {
|
||||
combineLatest,
|
||||
Observable,
|
||||
} from 'rxjs';
|
||||
import { map } from 'rxjs/operators';
|
||||
|
||||
import {
|
||||
AbstractMenuProvider,
|
||||
PartialMenuSection,
|
||||
} from '../../menu-provider.model';
|
||||
|
||||
/**
|
||||
* Helper provider for basic expandable menus
|
||||
*/
|
||||
export abstract class AbstractExpandableMenuProvider extends AbstractMenuProvider {
|
||||
|
||||
alwaysRenderExpandable = true;
|
||||
|
||||
/**
|
||||
* Get the top section for this expandable menu
|
||||
*/
|
||||
abstract getTopSection(): Observable<PartialMenuSection>;
|
||||
|
||||
/**
|
||||
* Get the subsections for this expandable menu
|
||||
*/
|
||||
abstract getSubSections(): Observable<PartialMenuSection[]>;
|
||||
|
||||
/**
|
||||
* Retrieve all sections
|
||||
* This method will combine both the top section and subsections
|
||||
*/
|
||||
getSections(): Observable<PartialMenuSection[]> {
|
||||
return combineLatest([
|
||||
this.getTopSection(),
|
||||
this.getSubSections(),
|
||||
]).pipe(
|
||||
map((
|
||||
[partialTopSection, partialSubSections]: [PartialMenuSection, PartialMenuSection[]],
|
||||
) => {
|
||||
const parentID = partialTopSection.id ?? this.getAutomatedSectionIdForTopSection();
|
||||
const subSections = partialSubSections.map((partialSub, index) => {
|
||||
return {
|
||||
...partialSub,
|
||||
id: partialSub.id ?? this.getAutomatedSectionIdForSubsection(index),
|
||||
parentID: parentID,
|
||||
alwaysRenderExpandable: false,
|
||||
};
|
||||
});
|
||||
|
||||
return [
|
||||
...subSections,
|
||||
{
|
||||
...partialTopSection,
|
||||
id: parentID,
|
||||
alwaysRenderExpandable: this.alwaysRenderExpandable,
|
||||
},
|
||||
];
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
protected getAutomatedSectionIdForTopSection(): string {
|
||||
return this.getAutomatedSectionId(0);
|
||||
}
|
||||
protected getAutomatedSectionIdForSubsection(indexOfSubSectionInProvider: number): string {
|
||||
return `${this.getAutomatedSectionIdForTopSection()}_${indexOfSubSectionInProvider}`;
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,102 @@
|
||||
/**
|
||||
* The contents of this file are subject to the license and copyright
|
||||
* detailed in the LICENSE and NOTICE files at the root of the source
|
||||
* tree and available online at
|
||||
*
|
||||
* http://www.dspace.org/license/
|
||||
*/
|
||||
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
import {
|
||||
ActivatedRouteSnapshot,
|
||||
RouterStateSnapshot,
|
||||
} from '@angular/router';
|
||||
import {
|
||||
Observable,
|
||||
of as observableOf,
|
||||
} from 'rxjs';
|
||||
|
||||
import { CacheableObject } from '../../../../core/cache/cacheable-object.model';
|
||||
import { MenuID } from '../../menu-id.model';
|
||||
import { MenuItemType } from '../../menu-item-type.model';
|
||||
import { PartialMenuSection } from '../../menu-provider.model';
|
||||
import { AbstractRouteContextMenuProvider } from './route-context.menu';
|
||||
|
||||
describe('AbstractRouteContextMenuProvider', () => {
|
||||
|
||||
class TestClass extends AbstractRouteContextMenuProvider<CacheableObject> {
|
||||
getRouteContext(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<CacheableObject> {
|
||||
return observableOf(object);
|
||||
}
|
||||
|
||||
getSectionsForContext(routeContext: CacheableObject): Observable<PartialMenuSection[]> {
|
||||
return observableOf(expectedSections);
|
||||
}
|
||||
}
|
||||
|
||||
const object = Object.assign(new CacheableObject());
|
||||
|
||||
|
||||
const expectedSections: PartialMenuSection[] = [
|
||||
{
|
||||
visible: true,
|
||||
model: {
|
||||
type: MenuItemType.TEXT,
|
||||
text: 'sub.section.test.1',
|
||||
},
|
||||
id: `${MenuID.ADMIN}_1_0`,
|
||||
parentID: `${MenuID.ADMIN}_1`,
|
||||
alwaysRenderExpandable: false,
|
||||
},
|
||||
{
|
||||
visible: true,
|
||||
model: {
|
||||
type: MenuItemType.TEXT,
|
||||
text: 'sub.section.test.2',
|
||||
},
|
||||
id: `${MenuID.ADMIN}_1_1`,
|
||||
parentID: `${MenuID.ADMIN}_1`,
|
||||
alwaysRenderExpandable: false,
|
||||
},
|
||||
{
|
||||
visible: true,
|
||||
model: {
|
||||
type: MenuItemType.TEXT,
|
||||
text: 'top.section.test',
|
||||
},
|
||||
icon: 'file-import',
|
||||
id: `${MenuID.ADMIN}_1`,
|
||||
alwaysRenderExpandable: true,
|
||||
},
|
||||
];
|
||||
|
||||
let provider: AbstractRouteContextMenuProvider<CacheableObject>;
|
||||
|
||||
beforeEach(() => {
|
||||
|
||||
TestBed.configureTestingModule({
|
||||
providers: [
|
||||
TestClass,
|
||||
],
|
||||
});
|
||||
provider = TestBed.inject(TestClass);
|
||||
provider.menuProviderId = `${MenuID.ADMIN}_1`;
|
||||
});
|
||||
|
||||
it('should be created', () => {
|
||||
expect(provider).toBeTruthy();
|
||||
});
|
||||
|
||||
it('getSections should return the sections based on the retrieved route context and sections for that context', (done) => {
|
||||
spyOn(provider, 'getRouteContext').and.callThrough();
|
||||
spyOn(provider, 'getSectionsForContext').and.callThrough();
|
||||
|
||||
provider.getSections(undefined, undefined).subscribe((sections) => {
|
||||
expect(sections).toEqual(expectedSections);
|
||||
expect(provider.getRouteContext).toHaveBeenCalled();
|
||||
expect(provider.getSectionsForContext).toHaveBeenCalledWith(object);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
});
|
@@ -0,0 +1,49 @@
|
||||
/**
|
||||
* The contents of this file are subject to the license and copyright
|
||||
* detailed in the LICENSE and NOTICE files at the root of the source
|
||||
* tree and available online at
|
||||
*
|
||||
* http://www.dspace.org/license/
|
||||
*/
|
||||
import {
|
||||
ActivatedRouteSnapshot,
|
||||
RouterStateSnapshot,
|
||||
} from '@angular/router';
|
||||
import {
|
||||
Observable,
|
||||
of as observableOf,
|
||||
} from 'rxjs';
|
||||
import { switchMap } from 'rxjs/operators';
|
||||
|
||||
import {
|
||||
AbstractMenuProvider,
|
||||
PartialMenuSection,
|
||||
} from '../../menu-provider.model';
|
||||
|
||||
/**
|
||||
* Helper provider for route dependent menus
|
||||
*/
|
||||
export abstract class AbstractRouteContextMenuProvider<T> extends AbstractMenuProvider {
|
||||
shouldPersistOnRouteChange = false;
|
||||
|
||||
abstract getRouteContext(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<T | undefined>;
|
||||
|
||||
abstract getSectionsForContext(routeContext: T): Observable<PartialMenuSection[]>;
|
||||
|
||||
getSections(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<PartialMenuSection[]> {
|
||||
|
||||
return this.getRouteContext(route, state).pipe(
|
||||
switchMap((routeContext: T) => {
|
||||
if (this.isApplicable(routeContext)) {
|
||||
return this.getSectionsForContext(routeContext);
|
||||
} else {
|
||||
return observableOf([]);
|
||||
}
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
protected isApplicable(routeContext: T): boolean {
|
||||
return true;
|
||||
}
|
||||
}
|
84
src/app/shared/menu/providers/import.menu.spec.ts
Normal file
84
src/app/shared/menu/providers/import.menu.spec.ts
Normal file
@@ -0,0 +1,84 @@
|
||||
/**
|
||||
* The contents of this file are subject to the license and copyright
|
||||
* detailed in the LICENSE and NOTICE files at the root of the source
|
||||
* tree and available online at
|
||||
*
|
||||
* http://www.dspace.org/license/
|
||||
*/
|
||||
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
import { of as observableOf } from 'rxjs';
|
||||
|
||||
import { AuthorizationDataService } from '../../../core/data/feature-authorization/authorization-data.service';
|
||||
import { ScriptDataService } from '../../../core/data/processes/script-data.service';
|
||||
import { AuthorizationDataServiceStub } from '../../testing/authorization-service.stub';
|
||||
import { ScriptServiceStub } from '../../testing/script-service.stub';
|
||||
import { MenuItemType } from '../menu-item-type.model';
|
||||
import { PartialMenuSection } from '../menu-provider.model';
|
||||
import { ImportMenuProvider } from './import.menu';
|
||||
|
||||
describe('ImportMenuProvider', () => {
|
||||
const expectedTopSection: PartialMenuSection = {
|
||||
visible: true,
|
||||
model: {
|
||||
type: MenuItemType.TEXT,
|
||||
text: 'menu.section.import',
|
||||
},
|
||||
icon: 'file-import',
|
||||
};
|
||||
|
||||
const expectedSubSections: PartialMenuSection[] = [
|
||||
{
|
||||
visible: true,
|
||||
model: {
|
||||
type: MenuItemType.LINK,
|
||||
text: 'menu.section.import_metadata',
|
||||
link: '/admin/metadata-import',
|
||||
},
|
||||
},
|
||||
{
|
||||
visible: true,
|
||||
model: {
|
||||
type: MenuItemType.LINK,
|
||||
text: 'menu.section.import_batch',
|
||||
link: '/admin/batch-import',
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
let provider: ImportMenuProvider;
|
||||
let authorizationServiceStub = new AuthorizationDataServiceStub();
|
||||
|
||||
beforeEach(() => {
|
||||
spyOn(authorizationServiceStub, 'isAuthorized').and.returnValue(
|
||||
observableOf(true),
|
||||
);
|
||||
|
||||
TestBed.configureTestingModule({
|
||||
providers: [
|
||||
ImportMenuProvider,
|
||||
{ provide: AuthorizationDataService, useValue: authorizationServiceStub },
|
||||
{ provide: ScriptDataService, useClass: ScriptServiceStub },
|
||||
],
|
||||
});
|
||||
provider = TestBed.inject(ImportMenuProvider);
|
||||
});
|
||||
|
||||
it('should be created', () => {
|
||||
expect(provider).toBeTruthy();
|
||||
});
|
||||
|
||||
it('getTopSection should return expected menu section', (done) => {
|
||||
provider.getTopSection().subscribe((section) => {
|
||||
expect(section).toEqual(expectedTopSection);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('getSubSections should return expected menu sections', (done) => {
|
||||
provider.getSubSections().subscribe((sections) => {
|
||||
expect(sections).toEqual(expectedSubSections);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
81
src/app/shared/menu/providers/import.menu.ts
Normal file
81
src/app/shared/menu/providers/import.menu.ts
Normal file
@@ -0,0 +1,81 @@
|
||||
/**
|
||||
* The contents of this file are subject to the license and copyright
|
||||
* detailed in the LICENSE and NOTICE files at the root of the source
|
||||
* tree and available online at
|
||||
*
|
||||
* http://www.dspace.org/license/
|
||||
*/
|
||||
|
||||
import { Injectable } from '@angular/core';
|
||||
import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
|
||||
import {
|
||||
combineLatest as observableCombineLatest,
|
||||
map,
|
||||
Observable,
|
||||
of as observableOf,
|
||||
} from 'rxjs';
|
||||
|
||||
import { AuthorizationDataService } from '../../../core/data/feature-authorization/authorization-data.service';
|
||||
import { FeatureID } from '../../../core/data/feature-authorization/feature-id';
|
||||
import {
|
||||
METADATA_IMPORT_SCRIPT_NAME,
|
||||
ScriptDataService,
|
||||
} from '../../../core/data/processes/script-data.service';
|
||||
import { MenuItemType } from '../menu-item-type.model';
|
||||
import { PartialMenuSection } from '../menu-provider.model';
|
||||
import { AbstractExpandableMenuProvider } from './helper-providers/expandable-menu-provider';
|
||||
|
||||
/**
|
||||
* Menu provider to create the "Import" menu (and subsections) in the admin sidebar
|
||||
*/
|
||||
@Injectable()
|
||||
export class ImportMenuProvider extends AbstractExpandableMenuProvider {
|
||||
constructor(
|
||||
protected authorizationService: AuthorizationDataService,
|
||||
protected scriptDataService: ScriptDataService,
|
||||
protected modalService: NgbModal,
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
public getTopSection(): Observable<PartialMenuSection> {
|
||||
return observableOf(
|
||||
{
|
||||
model: {
|
||||
type: MenuItemType.TEXT,
|
||||
text: 'menu.section.import',
|
||||
},
|
||||
icon: 'file-import',
|
||||
visible: true,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
public getSubSections(): Observable<PartialMenuSection[]> {
|
||||
return observableCombineLatest([
|
||||
this.authorizationService.isAuthorized(FeatureID.AdministratorOf),
|
||||
this.scriptDataService.scriptWithNameExistsAndCanExecute(METADATA_IMPORT_SCRIPT_NAME),
|
||||
]).pipe(
|
||||
map(([authorized, metadataImportScriptExists]) => {
|
||||
return [
|
||||
{
|
||||
visible: authorized && metadataImportScriptExists,
|
||||
model: {
|
||||
type: MenuItemType.LINK,
|
||||
text: 'menu.section.import_metadata',
|
||||
link: '/admin/metadata-import',
|
||||
},
|
||||
},
|
||||
{
|
||||
visible: authorized && metadataImportScriptExists,
|
||||
model: {
|
||||
type: MenuItemType.LINK,
|
||||
text: 'menu.section.import_batch',
|
||||
link: '/admin/batch-import',
|
||||
},
|
||||
},
|
||||
];
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
135
src/app/shared/menu/providers/item-claim.menu.spec.ts
Normal file
135
src/app/shared/menu/providers/item-claim.menu.spec.ts
Normal file
@@ -0,0 +1,135 @@
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { of as observableOf } from 'rxjs';
|
||||
|
||||
import { AuthorizationDataService } from '../../../core/data/feature-authorization/authorization-data.service';
|
||||
import { ResearcherProfileDataService } from '../../../core/profile/researcher-profile-data.service';
|
||||
import { Item } from '../../../core/shared/item.model';
|
||||
import { ITEM } from '../../../core/shared/item.resource-type';
|
||||
import { NotificationsService } from '../../notifications/notifications.service';
|
||||
import { NotificationsServiceStub } from '../../testing/notifications-service.stub';
|
||||
import { MenuService } from '../menu.service';
|
||||
import { MenuItemType } from '../menu-item-type.model';
|
||||
import { PartialMenuSection } from '../menu-provider.model';
|
||||
import { ClaimMenuProvider } from './item-claim.menu';
|
||||
|
||||
describe('ClaimMenuProvider', () => {
|
||||
|
||||
const expectedSections: PartialMenuSection[] = [
|
||||
{
|
||||
visible: true,
|
||||
model: {
|
||||
type: MenuItemType.ONCLICK,
|
||||
text: 'item.page.claim.button',
|
||||
function: jasmine.any(Function) as any,
|
||||
},
|
||||
icon: 'hand-paper',
|
||||
},
|
||||
];
|
||||
|
||||
let provider: ClaimMenuProvider;
|
||||
|
||||
const item: Item = Object.assign(new Item(), {
|
||||
type: ITEM.value,
|
||||
_links: { self: { href: 'self-link' } },
|
||||
metadata: {
|
||||
'dc.title': [{
|
||||
'value': 'Untyped Item',
|
||||
}],
|
||||
},
|
||||
|
||||
});
|
||||
const person: Item = Object.assign(new Item(), {
|
||||
type: ITEM.value,
|
||||
_links: { self: { href: 'self-link' } },
|
||||
metadata: {
|
||||
'dc.title': [{
|
||||
'value': 'Person Entity',
|
||||
}],
|
||||
'dspace.entity.type': [{
|
||||
'value': 'Person',
|
||||
}],
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
let authorizationService;
|
||||
let menuService;
|
||||
let notificationsService;
|
||||
let researcherProfileService;
|
||||
let modalService;
|
||||
|
||||
|
||||
beforeEach(() => {
|
||||
|
||||
authorizationService = jasmine.createSpyObj('authorizationService', {
|
||||
'isAuthorized': observableOf(true),
|
||||
'invalidateAuthorizationsRequestCache': {},
|
||||
});
|
||||
|
||||
menuService = jasmine.createSpyObj('menuService', ['hideMenuSection']);
|
||||
|
||||
notificationsService = new NotificationsServiceStub();
|
||||
|
||||
researcherProfileService = jasmine.createSpyObj('authorizationService', {
|
||||
'createFromExternalSourceAndReturnRelatedItemId': observableOf('profile-id'),
|
||||
});
|
||||
|
||||
modalService = jasmine.createSpyObj('modalService', ['open']);
|
||||
|
||||
|
||||
TestBed.configureTestingModule({
|
||||
imports: [TranslateModule.forRoot()],
|
||||
providers: [
|
||||
ClaimMenuProvider,
|
||||
{ provide: AuthorizationDataService, useValue: authorizationService },
|
||||
{ provide: MenuService, useValue: menuService },
|
||||
{ provide: NotificationsService, useValue: notificationsService },
|
||||
{ provide: ResearcherProfileDataService, useValue: researcherProfileService },
|
||||
{ provide: NgbModal, useValue: modalService },
|
||||
],
|
||||
});
|
||||
provider = TestBed.inject(ClaimMenuProvider);
|
||||
});
|
||||
|
||||
it('should be created', () => {
|
||||
expect(provider).toBeTruthy();
|
||||
});
|
||||
|
||||
describe('getSectionsForContext', () => {
|
||||
it('should return the expected sections', (done) => {
|
||||
provider.getSectionsForContext(person).subscribe((sections) => {
|
||||
expect(sections).toEqual(expectedSections);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('isApplicable', () => {
|
||||
it('should return true whe the provided dspace object is a person entity', () => {
|
||||
const result = (provider as any).isApplicable(person);
|
||||
expect(result).toBeTrue();
|
||||
});
|
||||
it('should return true whe the provided dspace object is not a person entity', () => {
|
||||
const result = (provider as any).isApplicable(item);
|
||||
expect(result).toBeFalse();
|
||||
});
|
||||
});
|
||||
|
||||
describe('claimResearcher', () => {
|
||||
it('should show a success notification and hide the menu when an id is returned by the researcher profile service', () => {
|
||||
(provider as any).claimResearcher(person);
|
||||
expect(notificationsService.success).toHaveBeenCalled();
|
||||
expect(authorizationService.invalidateAuthorizationsRequestCache).toHaveBeenCalled();
|
||||
expect(menuService.hideMenuSection).toHaveBeenCalled();
|
||||
});
|
||||
it('should show an error notification when no id is returned by the researcher profile service', () => {
|
||||
(researcherProfileService.createFromExternalSourceAndReturnRelatedItemId as jasmine.Spy).and.returnValue(observableOf(null));
|
||||
(provider as any).claimResearcher(person);
|
||||
expect(notificationsService.error).toHaveBeenCalled();
|
||||
expect(authorizationService.invalidateAuthorizationsRequestCache).not.toHaveBeenCalled();
|
||||
expect(menuService.hideMenuSection).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
98
src/app/shared/menu/providers/item-claim.menu.ts
Normal file
98
src/app/shared/menu/providers/item-claim.menu.ts
Normal file
@@ -0,0 +1,98 @@
|
||||
/**
|
||||
* The contents of this file are subject to the license and copyright
|
||||
* detailed in the LICENSE and NOTICE files at the root of the source
|
||||
* tree and available online at
|
||||
*
|
||||
* http://www.dspace.org/license/
|
||||
*/
|
||||
import { Injectable } from '@angular/core';
|
||||
import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import {
|
||||
combineLatest,
|
||||
Observable,
|
||||
} from 'rxjs';
|
||||
import { map } from 'rxjs/operators';
|
||||
|
||||
import { AuthorizationDataService } from '../../../core/data/feature-authorization/authorization-data.service';
|
||||
import { FeatureID } from '../../../core/data/feature-authorization/feature-id';
|
||||
import { ResearcherProfileDataService } from '../../../core/profile/researcher-profile-data.service';
|
||||
import { DSpaceObject } from '../../../core/shared/dspace-object.model';
|
||||
import { Item } from '../../../core/shared/item.model';
|
||||
import { isNotEmpty } from '../../empty.util';
|
||||
import { NotificationsService } from '../../notifications/notifications.service';
|
||||
import { MenuService } from '../menu.service';
|
||||
import { MenuID } from '../menu-id.model';
|
||||
import { OnClickMenuItemModel } from '../menu-item/models/onclick.model';
|
||||
import { MenuItemType } from '../menu-item-type.model';
|
||||
import { PartialMenuSection } from '../menu-provider.model';
|
||||
import { DSpaceObjectPageMenuProvider } from './helper-providers/dso.menu';
|
||||
|
||||
/**
|
||||
* Menu provider to create the "Claim" option in the DSO edit menu on person entity pages.
|
||||
* This option allows to claim a researcher by creating a profile.
|
||||
*/
|
||||
@Injectable()
|
||||
export class ClaimMenuProvider extends DSpaceObjectPageMenuProvider {
|
||||
constructor(
|
||||
protected authorizationService: AuthorizationDataService,
|
||||
protected menuService: MenuService,
|
||||
protected translate: TranslateService,
|
||||
protected notificationsService: NotificationsService,
|
||||
protected researcherProfileService: ResearcherProfileDataService,
|
||||
protected modalService: NgbModal,
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
public getSectionsForContext(item: Item): Observable<PartialMenuSection[]> {
|
||||
return combineLatest([
|
||||
this.authorizationService.isAuthorized(FeatureID.CanClaimItem, item.self),
|
||||
]).pipe(
|
||||
map(([canClaimItem]) => {
|
||||
return [
|
||||
{
|
||||
visible: canClaimItem,
|
||||
model: {
|
||||
type: MenuItemType.ONCLICK,
|
||||
text: 'item.page.claim.button',
|
||||
function: () => {
|
||||
this.claimResearcher(item);
|
||||
},
|
||||
} as OnClickMenuItemModel,
|
||||
icon: 'hand-paper',
|
||||
},
|
||||
] as PartialMenuSection[];
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
protected isApplicable(item: DSpaceObject): boolean {
|
||||
if (item instanceof Item) {
|
||||
return this.getDsoType(item) === 'person';
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Claim a researcher by creating a profile
|
||||
* Shows notifications and/or hides the menu section on success/error
|
||||
*/
|
||||
protected claimResearcher(item: Item) {
|
||||
this.researcherProfileService.createFromExternalSourceAndReturnRelatedItemId(item.self).subscribe((id: string) => {
|
||||
if (isNotEmpty(id)) {
|
||||
this.notificationsService.success(
|
||||
this.translate.get('researcherprofile.success.claim.title'),
|
||||
this.translate.get('researcherprofile.success.claim.body'),
|
||||
);
|
||||
this.authorizationService.invalidateAuthorizationsRequestCache();
|
||||
this.menuService.hideMenuSection(MenuID.DSO_EDIT, this.getAutomatedSectionId(0));
|
||||
} else {
|
||||
this.notificationsService.error(
|
||||
this.translate.get('researcherprofile.error.claim.title'),
|
||||
this.translate.get('researcherprofile.error.claim.body'),
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
95
src/app/shared/menu/providers/item-orcid.menu.spec.ts
Normal file
95
src/app/shared/menu/providers/item-orcid.menu.spec.ts
Normal file
@@ -0,0 +1,95 @@
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { of as observableOf } from 'rxjs';
|
||||
|
||||
import { AuthorizationDataService } from '../../../core/data/feature-authorization/authorization-data.service';
|
||||
import { Item } from '../../../core/shared/item.model';
|
||||
import { ITEM } from '../../../core/shared/item.resource-type';
|
||||
import { URLCombiner } from '../../../core/url-combiner/url-combiner';
|
||||
import { MenuItemType } from '../menu-item-type.model';
|
||||
import { PartialMenuSection } from '../menu-provider.model';
|
||||
import { OrcidMenuProvider } from './item-orcid.menu';
|
||||
|
||||
describe('OrcidMenuProvider', () => {
|
||||
|
||||
const expectedSections: PartialMenuSection[] = [
|
||||
{
|
||||
visible: true,
|
||||
model: {
|
||||
type: MenuItemType.LINK,
|
||||
text: 'item.page.orcid.tooltip',
|
||||
link: new URLCombiner('/entities/person/test-uuid', 'orcid').toString(),
|
||||
},
|
||||
icon: 'orcid fab fa-lg',
|
||||
},
|
||||
];
|
||||
|
||||
let provider: OrcidMenuProvider;
|
||||
|
||||
const item: Item = Object.assign(new Item(), {
|
||||
type: ITEM.value,
|
||||
_links: { self: { href: 'self-link' } },
|
||||
metadata: {
|
||||
'dc.title': [{
|
||||
'value': 'Untyped Item',
|
||||
}],
|
||||
},
|
||||
});
|
||||
|
||||
const person: Item = Object.assign(new Item(), {
|
||||
uuid: 'test-uuid',
|
||||
type: ITEM.value,
|
||||
_links: { self: { href: 'self-link' } },
|
||||
metadata: {
|
||||
'dc.title': [{
|
||||
'value': 'Person Entity',
|
||||
}],
|
||||
'dspace.entity.type': [{
|
||||
'value': 'Person',
|
||||
}],
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
let authorizationService;
|
||||
|
||||
beforeEach(() => {
|
||||
authorizationService = jasmine.createSpyObj('authorizationService', {
|
||||
'isAuthorized': observableOf(true),
|
||||
});
|
||||
|
||||
TestBed.configureTestingModule({
|
||||
imports: [TranslateModule.forRoot()],
|
||||
providers: [
|
||||
OrcidMenuProvider,
|
||||
{ provide: AuthorizationDataService, useValue: authorizationService },
|
||||
],
|
||||
});
|
||||
provider = TestBed.inject(OrcidMenuProvider);
|
||||
});
|
||||
|
||||
it('should be created', () => {
|
||||
expect(provider).toBeTruthy();
|
||||
});
|
||||
|
||||
describe('getSectionsForContext', () => {
|
||||
it('should return the expected sections', (done) => {
|
||||
provider.getSectionsForContext(person).subscribe((sections) => {
|
||||
expect(sections).toEqual(expectedSections);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('isApplicable', () => {
|
||||
it('should return true whe the provided dspace object is a person entity', () => {
|
||||
const result = (provider as any).isApplicable(person);
|
||||
expect(result).toBeTrue();
|
||||
});
|
||||
it('should return true whe the provided dspace object is not a person entity', () => {
|
||||
const result = (provider as any).isApplicable(item);
|
||||
expect(result).toBeFalse();
|
||||
});
|
||||
});
|
||||
|
||||
});
|
62
src/app/shared/menu/providers/item-orcid.menu.ts
Normal file
62
src/app/shared/menu/providers/item-orcid.menu.ts
Normal file
@@ -0,0 +1,62 @@
|
||||
/**
|
||||
* The contents of this file are subject to the license and copyright
|
||||
* detailed in the LICENSE and NOTICE files at the root of the source
|
||||
* tree and available online at
|
||||
*
|
||||
* http://www.dspace.org/license/
|
||||
*/
|
||||
import { Injectable } from '@angular/core';
|
||||
import {
|
||||
combineLatest,
|
||||
Observable,
|
||||
} from 'rxjs';
|
||||
import { map } from 'rxjs/operators';
|
||||
|
||||
import { getDSORoute } from '../../../app-routing-paths';
|
||||
import { AuthorizationDataService } from '../../../core/data/feature-authorization/authorization-data.service';
|
||||
import { FeatureID } from '../../../core/data/feature-authorization/feature-id';
|
||||
import { Item } from '../../../core/shared/item.model';
|
||||
import { URLCombiner } from '../../../core/url-combiner/url-combiner';
|
||||
import { LinkMenuItemModel } from '../menu-item/models/link.model';
|
||||
import { MenuItemType } from '../menu-item-type.model';
|
||||
import { PartialMenuSection } from '../menu-provider.model';
|
||||
import { DSpaceObjectPageMenuProvider } from './helper-providers/dso.menu';
|
||||
|
||||
/**
|
||||
* Menu provider to create the Orcid settings related option in the DSO edit menu on person entity pages
|
||||
*/
|
||||
@Injectable()
|
||||
export class OrcidMenuProvider extends DSpaceObjectPageMenuProvider {
|
||||
constructor(
|
||||
protected authorizationService: AuthorizationDataService,
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
public getSectionsForContext(item: Item): Observable<PartialMenuSection[]> {
|
||||
return combineLatest([
|
||||
this.authorizationService.isAuthorized(FeatureID.CanSynchronizeWithORCID, item.self),
|
||||
]).pipe(
|
||||
map(([canSynchronizeWithOrcid]) => {
|
||||
return [
|
||||
{
|
||||
visible: canSynchronizeWithOrcid,
|
||||
model: {
|
||||
type: MenuItemType.LINK,
|
||||
text: 'item.page.orcid.tooltip',
|
||||
link: new URLCombiner(getDSORoute(item), 'orcid').toString(),
|
||||
} as LinkMenuItemModel,
|
||||
icon: 'orcid fab fa-lg',
|
||||
},
|
||||
] as PartialMenuSection[];
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
protected isApplicable(item: Item): boolean {
|
||||
if (item instanceof Item) {
|
||||
return this.getDsoType(item) === 'person';
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
99
src/app/shared/menu/providers/item-versioning.menu.spec.ts
Normal file
99
src/app/shared/menu/providers/item-versioning.menu.spec.ts
Normal file
@@ -0,0 +1,99 @@
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { of as observableOf } from 'rxjs';
|
||||
|
||||
import { AuthorizationDataService } from '../../../core/data/feature-authorization/authorization-data.service';
|
||||
import { Item } from '../../../core/shared/item.model';
|
||||
import { ITEM } from '../../../core/shared/item.resource-type';
|
||||
import { DsoVersioningModalService } from '../../dso-page/dso-versioning-modal-service/dso-versioning-modal.service';
|
||||
import { MenuItemType } from '../menu-item-type.model';
|
||||
import { PartialMenuSection } from '../menu-provider.model';
|
||||
import { VersioningMenuProvider } from './item-versioning.menu';
|
||||
|
||||
describe('VersioningMenuProvider', () => {
|
||||
|
||||
const expectedSectionsWhenVersionNotPresent: PartialMenuSection[] = [
|
||||
{
|
||||
visible: true,
|
||||
model: {
|
||||
type: MenuItemType.ONCLICK,
|
||||
text: 'item.page.version.create',
|
||||
disabled: false,
|
||||
function: jasmine.any(Function) as any,
|
||||
},
|
||||
icon: 'code-branch',
|
||||
},
|
||||
];
|
||||
const expectedSectionsWhenVersionPresent: PartialMenuSection[] = [
|
||||
{
|
||||
visible: true,
|
||||
model: {
|
||||
type: MenuItemType.ONCLICK,
|
||||
text: 'item.page.version.hasDraft',
|
||||
disabled: true,
|
||||
function: jasmine.any(Function) as any,
|
||||
},
|
||||
icon: 'code-branch',
|
||||
},
|
||||
];
|
||||
|
||||
let provider: VersioningMenuProvider;
|
||||
|
||||
const item: Item = Object.assign(new Item(), {
|
||||
type: ITEM.value,
|
||||
_links: { self: { href: 'self-link' } },
|
||||
metadata: {
|
||||
'dc.title': [{
|
||||
'value': 'Untyped Item',
|
||||
}],
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
let authorizationService;
|
||||
let dsoVersioningModalService;
|
||||
|
||||
beforeEach(() => {
|
||||
authorizationService = jasmine.createSpyObj('authorizationService', {
|
||||
isAuthorized: observableOf(true),
|
||||
});
|
||||
|
||||
dsoVersioningModalService = jasmine.createSpyObj('dsoVersioningModalService', {
|
||||
isNewVersionButtonDisabled: observableOf(false),
|
||||
getVersioningTooltipMessage: observableOf('item.page.version.create'),
|
||||
});
|
||||
|
||||
TestBed.configureTestingModule({
|
||||
imports: [TranslateModule.forRoot()],
|
||||
providers: [
|
||||
VersioningMenuProvider,
|
||||
{ provide: AuthorizationDataService, useValue: authorizationService },
|
||||
{ provide: DsoVersioningModalService, useValue: dsoVersioningModalService },
|
||||
],
|
||||
});
|
||||
provider = TestBed.inject(VersioningMenuProvider);
|
||||
});
|
||||
|
||||
it('should be created', () => {
|
||||
expect(provider).toBeTruthy();
|
||||
});
|
||||
|
||||
describe('getSectionsForContext', () => {
|
||||
it('should return the section to create a new version when no version draft is present yet', (done) => {
|
||||
provider.getSectionsForContext(item).subscribe((sections) => {
|
||||
expect(sections).toEqual(expectedSectionsWhenVersionNotPresent);
|
||||
done();
|
||||
});
|
||||
});
|
||||
it('should return the section to that a version is present when a version draft is present', (done) => {
|
||||
(dsoVersioningModalService.isNewVersionButtonDisabled as jasmine.Spy).and.returnValue(observableOf(true));
|
||||
(dsoVersioningModalService.getVersioningTooltipMessage as jasmine.Spy).and.returnValue(observableOf('item.page.version.hasDraft'));
|
||||
|
||||
provider.getSectionsForContext(item).subscribe((sections) => {
|
||||
expect(sections).toEqual(expectedSectionsWhenVersionPresent);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
});
|
63
src/app/shared/menu/providers/item-versioning.menu.ts
Normal file
63
src/app/shared/menu/providers/item-versioning.menu.ts
Normal file
@@ -0,0 +1,63 @@
|
||||
/**
|
||||
* The contents of this file are subject to the license and copyright
|
||||
* detailed in the LICENSE and NOTICE files at the root of the source
|
||||
* tree and available online at
|
||||
*
|
||||
* http://www.dspace.org/license/
|
||||
*/
|
||||
import { Injectable } from '@angular/core';
|
||||
import {
|
||||
combineLatest,
|
||||
Observable,
|
||||
} from 'rxjs';
|
||||
import { map } from 'rxjs/operators';
|
||||
|
||||
import { AuthorizationDataService } from '../../../core/data/feature-authorization/authorization-data.service';
|
||||
import { FeatureID } from '../../../core/data/feature-authorization/feature-id';
|
||||
import { Item } from '../../../core/shared/item.model';
|
||||
import { DsoVersioningModalService } from '../../dso-page/dso-versioning-modal-service/dso-versioning-modal.service';
|
||||
import { OnClickMenuItemModel } from '../menu-item/models/onclick.model';
|
||||
import { MenuItemType } from '../menu-item-type.model';
|
||||
import { PartialMenuSection } from '../menu-provider.model';
|
||||
import { DSpaceObjectPageMenuProvider } from './helper-providers/dso.menu';
|
||||
|
||||
/**
|
||||
* Menu provider to create the version related options in the DSO edit menu
|
||||
*/
|
||||
@Injectable()
|
||||
export class VersioningMenuProvider extends DSpaceObjectPageMenuProvider {
|
||||
constructor(
|
||||
protected authorizationService: AuthorizationDataService,
|
||||
protected dsoVersioningModalService: DsoVersioningModalService,
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
public getSectionsForContext(item: Item): Observable<PartialMenuSection[]> {
|
||||
|
||||
|
||||
return combineLatest([
|
||||
this.authorizationService.isAuthorized(FeatureID.CanCreateVersion, item.self),
|
||||
this.dsoVersioningModalService.isNewVersionButtonDisabled(item),
|
||||
this.dsoVersioningModalService.getVersioningTooltipMessage(item, 'item.page.version.hasDraft', 'item.page.version.create'),
|
||||
]).pipe(
|
||||
map(([canCreateVersion, disableVersioning, versionTooltip]) => {
|
||||
|
||||
return [
|
||||
{
|
||||
visible: canCreateVersion,
|
||||
model: {
|
||||
type: MenuItemType.ONCLICK,
|
||||
text: versionTooltip,
|
||||
disabled: disableVersioning,
|
||||
function: () => {
|
||||
this.dsoVersioningModalService.openCreateVersionModal(item);
|
||||
},
|
||||
} as OnClickMenuItemModel,
|
||||
icon: 'code-branch',
|
||||
},
|
||||
] as PartialMenuSection[];
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
113
src/app/shared/menu/providers/new.menu.spec.ts
Normal file
113
src/app/shared/menu/providers/new.menu.spec.ts
Normal file
@@ -0,0 +1,113 @@
|
||||
/**
|
||||
* The contents of this file are subject to the license and copyright
|
||||
* detailed in the LICENSE and NOTICE files at the root of the source
|
||||
* tree and available online at
|
||||
*
|
||||
* http://www.dspace.org/license/
|
||||
*/
|
||||
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
import { of as observableOf } from 'rxjs';
|
||||
|
||||
import { AuthorizationDataService } from '../../../core/data/feature-authorization/authorization-data.service';
|
||||
import { FeatureID } from '../../../core/data/feature-authorization/feature-id';
|
||||
import { AuthorizationDataServiceStub } from '../../testing/authorization-service.stub';
|
||||
import { LinkMenuItemModel } from '../menu-item/models/link.model';
|
||||
import { MenuItemType } from '../menu-item-type.model';
|
||||
import { PartialMenuSection } from '../menu-provider.model';
|
||||
import { NewMenuProvider } from './new.menu';
|
||||
|
||||
describe('NewMenuProvider', () => {
|
||||
const expectedTopSection: PartialMenuSection = {
|
||||
accessibilityHandle: 'new',
|
||||
visible: true,
|
||||
model: {
|
||||
type: MenuItemType.TEXT,
|
||||
text: 'menu.section.new',
|
||||
},
|
||||
icon: 'plus',
|
||||
};
|
||||
|
||||
const expectedSubSections: PartialMenuSection[] = [
|
||||
{
|
||||
visible: true,
|
||||
model: {
|
||||
type: MenuItemType.ONCLICK,
|
||||
text: 'menu.section.new_community',
|
||||
function: jasmine.any(Function) as any,
|
||||
},
|
||||
},
|
||||
{
|
||||
visible: false,
|
||||
model: {
|
||||
type: MenuItemType.ONCLICK,
|
||||
text: 'menu.section.new_collection',
|
||||
function: jasmine.any(Function) as any,
|
||||
},
|
||||
},
|
||||
{
|
||||
visible: true,
|
||||
model: {
|
||||
type: MenuItemType.ONCLICK,
|
||||
text: 'menu.section.new_item',
|
||||
function: jasmine.any(Function) as any,
|
||||
},
|
||||
},
|
||||
{
|
||||
visible: true,
|
||||
model: {
|
||||
type: MenuItemType.LINK,
|
||||
text: 'menu.section.new_process',
|
||||
link: '/processes/new',
|
||||
},
|
||||
},
|
||||
{
|
||||
visible: true,
|
||||
model: {
|
||||
type: MenuItemType.LINK,
|
||||
text: 'menu.section.services_new',
|
||||
link: '/admin/ldn/services/new',
|
||||
} as LinkMenuItemModel,
|
||||
icon: '',
|
||||
},
|
||||
];
|
||||
|
||||
let provider: NewMenuProvider;
|
||||
let authorizationServiceStub = new AuthorizationDataServiceStub();
|
||||
|
||||
beforeEach(() => {
|
||||
spyOn(authorizationServiceStub, 'isAuthorized').and.callFake((id: FeatureID) => {
|
||||
if (id === FeatureID.IsCollectionAdmin) {
|
||||
return observableOf(false);
|
||||
} else {
|
||||
return observableOf(true);
|
||||
}
|
||||
});
|
||||
|
||||
TestBed.configureTestingModule({
|
||||
providers: [
|
||||
NewMenuProvider,
|
||||
{ provide: AuthorizationDataService, useValue: authorizationServiceStub },
|
||||
],
|
||||
});
|
||||
provider = TestBed.inject(NewMenuProvider);
|
||||
});
|
||||
|
||||
it('should be created', () => {
|
||||
expect(provider).toBeTruthy();
|
||||
});
|
||||
|
||||
it('getTopSection should return expected menu section', (done) => {
|
||||
provider.getTopSection().subscribe((section) => {
|
||||
expect(section).toEqual(expectedTopSection);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('getSubSections should return expected menu sections', (done) => {
|
||||
provider.getSubSections().subscribe((sections) => {
|
||||
expect(sections).toEqual(expectedSubSections);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
115
src/app/shared/menu/providers/new.menu.ts
Normal file
115
src/app/shared/menu/providers/new.menu.ts
Normal file
@@ -0,0 +1,115 @@
|
||||
/**
|
||||
* The contents of this file are subject to the license and copyright
|
||||
* detailed in the LICENSE and NOTICE files at the root of the source
|
||||
* tree and available online at
|
||||
*
|
||||
* http://www.dspace.org/license/
|
||||
*/
|
||||
|
||||
import { Injectable } from '@angular/core';
|
||||
import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
|
||||
import {
|
||||
combineLatest,
|
||||
map,
|
||||
Observable,
|
||||
of as observableOf,
|
||||
} from 'rxjs';
|
||||
|
||||
import { AuthorizationDataService } from '../../../core/data/feature-authorization/authorization-data.service';
|
||||
import { FeatureID } from '../../../core/data/feature-authorization/feature-id';
|
||||
import { ThemedCreateCollectionParentSelectorComponent } from '../../dso-selector/modal-wrappers/create-collection-parent-selector/themed-create-collection-parent-selector.component';
|
||||
import { ThemedCreateCommunityParentSelectorComponent } from '../../dso-selector/modal-wrappers/create-community-parent-selector/themed-create-community-parent-selector.component';
|
||||
import { ThemedCreateItemParentSelectorComponent } from '../../dso-selector/modal-wrappers/create-item-parent-selector/themed-create-item-parent-selector.component';
|
||||
import { LinkMenuItemModel } from '../menu-item/models/link.model';
|
||||
import { TextMenuItemModel } from '../menu-item/models/text.model';
|
||||
import { MenuItemType } from '../menu-item-type.model';
|
||||
import { PartialMenuSection } from '../menu-provider.model';
|
||||
import { AbstractExpandableMenuProvider } from './helper-providers/expandable-menu-provider';
|
||||
|
||||
/**
|
||||
* Menu provider to create the "New" menu (and subsections) in the admin sidebar
|
||||
*/
|
||||
@Injectable()
|
||||
export class NewMenuProvider extends AbstractExpandableMenuProvider {
|
||||
constructor(
|
||||
protected authorizationService: AuthorizationDataService,
|
||||
protected modalService: NgbModal,
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
public getTopSection(): Observable<PartialMenuSection> {
|
||||
return observableOf(
|
||||
{
|
||||
accessibilityHandle: 'new',
|
||||
model: {
|
||||
type: MenuItemType.TEXT,
|
||||
text: 'menu.section.new',
|
||||
} as TextMenuItemModel,
|
||||
icon: 'plus',
|
||||
visible: true,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
public getSubSections(): Observable<PartialMenuSection[]> {
|
||||
return combineLatest([
|
||||
this.authorizationService.isAuthorized(FeatureID.IsCollectionAdmin),
|
||||
this.authorizationService.isAuthorized(FeatureID.IsCommunityAdmin),
|
||||
this.authorizationService.isAuthorized(FeatureID.AdministratorOf),
|
||||
this.authorizationService.isAuthorized(FeatureID.CanSubmit),
|
||||
this.authorizationService.isAuthorized(FeatureID.CoarNotifyEnabled),
|
||||
]).pipe(map(([isCollectionAdmin, isCommunityAdmin, isSiteAdmin, canSubmit, isCoarNotifyEnabled]: [boolean, boolean, boolean, boolean, boolean]) => {
|
||||
|
||||
return [
|
||||
{
|
||||
visible: isCommunityAdmin,
|
||||
model: {
|
||||
type: MenuItemType.ONCLICK,
|
||||
text: 'menu.section.new_community',
|
||||
function: () => {
|
||||
this.modalService.open(ThemedCreateCommunityParentSelectorComponent);
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
visible: isCollectionAdmin,
|
||||
model: {
|
||||
type: MenuItemType.ONCLICK,
|
||||
text: 'menu.section.new_collection',
|
||||
function: () => {
|
||||
this.modalService.open(ThemedCreateCollectionParentSelectorComponent);
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
visible: canSubmit,
|
||||
model: {
|
||||
type: MenuItemType.ONCLICK,
|
||||
text: 'menu.section.new_item',
|
||||
function: () => {
|
||||
this.modalService.open(ThemedCreateItemParentSelectorComponent);
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
visible: isSiteAdmin,
|
||||
model: {
|
||||
type: MenuItemType.LINK,
|
||||
text: 'menu.section.new_process',
|
||||
link: '/processes/new',
|
||||
},
|
||||
},
|
||||
{
|
||||
visible: isSiteAdmin && isCoarNotifyEnabled,
|
||||
model: {
|
||||
type: MenuItemType.LINK,
|
||||
text: 'menu.section.services_new',
|
||||
link: '/admin/ldn/services/new',
|
||||
} as LinkMenuItemModel,
|
||||
icon: '',
|
||||
},
|
||||
];
|
||||
}));
|
||||
}
|
||||
}
|
89
src/app/shared/menu/providers/notifications.menu.spec.ts
Normal file
89
src/app/shared/menu/providers/notifications.menu.spec.ts
Normal file
@@ -0,0 +1,89 @@
|
||||
/**
|
||||
* The contents of this file are subject to the license and copyright
|
||||
* detailed in the LICENSE and NOTICE files at the root of the source
|
||||
* tree and available online at
|
||||
*
|
||||
* http://www.dspace.org/license/
|
||||
*/
|
||||
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
import { of as observableOf } from 'rxjs';
|
||||
|
||||
import { PUBLICATION_CLAIMS_PATH } from '../../../admin/admin-notifications/admin-notifications-routing-paths';
|
||||
import { AuthorizationDataService } from '../../../core/data/feature-authorization/authorization-data.service';
|
||||
import { FeatureID } from '../../../core/data/feature-authorization/feature-id';
|
||||
import { AuthorizationDataServiceStub } from '../../testing/authorization-service.stub';
|
||||
import { LinkMenuItemModel } from '../menu-item/models/link.model';
|
||||
import { TextMenuItemModel } from '../menu-item/models/text.model';
|
||||
import { MenuItemType } from '../menu-item-type.model';
|
||||
import { PartialMenuSection } from '../menu-provider.model';
|
||||
import { NotificationsMenuProvider } from './notifications.menu';
|
||||
|
||||
describe('NotificationsMenuProvider', () => {
|
||||
const expectedTopSection: PartialMenuSection = {
|
||||
visible: true,
|
||||
model: {
|
||||
type: MenuItemType.TEXT,
|
||||
text: 'menu.section.notifications',
|
||||
} as TextMenuItemModel,
|
||||
icon: 'bell',
|
||||
};
|
||||
|
||||
const expectedSubSections: PartialMenuSection[] = [
|
||||
{
|
||||
visible: true,
|
||||
model: {
|
||||
type: MenuItemType.LINK,
|
||||
text: 'menu.section.quality-assurance',
|
||||
link: '/notifications/quality-assurance',
|
||||
} as LinkMenuItemModel,
|
||||
},
|
||||
{
|
||||
visible: true,
|
||||
model: {
|
||||
type: MenuItemType.LINK,
|
||||
text: 'menu.section.notifications_publication-claim',
|
||||
link: '/admin/notifications/' + PUBLICATION_CLAIMS_PATH,
|
||||
} as LinkMenuItemModel,
|
||||
},
|
||||
];
|
||||
|
||||
let provider: NotificationsMenuProvider;
|
||||
let authorizationServiceStub = new AuthorizationDataServiceStub();
|
||||
|
||||
beforeEach(() => {
|
||||
spyOn(authorizationServiceStub, 'isAuthorized').and.callFake((id: FeatureID) => {
|
||||
if (id === FeatureID.CanSeeQA || id === FeatureID.AdministratorOf) {
|
||||
return observableOf(true);
|
||||
} else {
|
||||
return observableOf(false);
|
||||
}
|
||||
});
|
||||
|
||||
TestBed.configureTestingModule({
|
||||
providers: [
|
||||
NotificationsMenuProvider,
|
||||
{ provide: AuthorizationDataService, useValue: authorizationServiceStub },
|
||||
],
|
||||
});
|
||||
provider = TestBed.inject(NotificationsMenuProvider);
|
||||
});
|
||||
|
||||
it('should be created', () => {
|
||||
expect(provider).toBeTruthy();
|
||||
});
|
||||
|
||||
it('getTopSection should return expected menu section', (done) => {
|
||||
provider.getTopSection().subscribe((section) => {
|
||||
expect(section).toEqual(expectedTopSection);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('getSubSections should return expected menu sections', (done) => {
|
||||
provider.getSubSections().subscribe((sections) => {
|
||||
expect(sections).toEqual(expectedSubSections);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user