Finalise menu refactor, add typedocs and tests

This commit is contained in:
Yana De Pauw
2024-11-15 18:25:37 +01:00
parent 5a7ebd4ba9
commit a105131b2b
50 changed files with 668 additions and 2155 deletions

View File

@@ -12,59 +12,108 @@ import { By } from '@angular/platform-browser';
import { TranslateModule } from '@ngx-translate/core';
import { Router } from '@angular/router';
import { RouterStub } from '../../../shared/testing/router.stub';
import { MenuItemModels } from '../../../shared/menu/menu-section.model';
describe('ExpandableAdminSidebarSectionComponent', () => {
let component: ExpandableAdminSidebarSectionComponent;
let fixture: ComponentFixture<ExpandableAdminSidebarSectionComponent>;
const menuService = new MenuServiceStub();
const iconString = 'test';
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
imports: [NoopAnimationsModule, TranslateModule.forRoot()],
declarations: [ExpandableAdminSidebarSectionComponent, TestComponent],
providers: [
{ provide: 'sectionDataProvider', useValue: { icon: iconString, model: {} } },
{ provide: MenuService, useValue: menuService },
{ provide: CSSVariableService, useClass: CSSVariableServiceStub },
{ provide: Router, useValue: new RouterStub() },
]
}).overrideComponent(ExpandableAdminSidebarSectionComponent, {
set: {
entryComponents: [TestComponent]
}
})
.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 set the right icon', () => {
const icon = fixture.debugElement.query(By.css('.shortcut-icon > i.fas'));
expect(icon.nativeElement.getAttribute('class')).toContain('fa-' + iconString);
});
describe('when the header text is clicked', () => {
beforeEach(() => {
spyOn(menuService, 'toggleActiveSection');
const sidebarToggler = fixture.debugElement.query(By.css('.sidebar-section > div.nav-item'));
sidebarToggler.triggerEventHandler('click', {
preventDefault: () => {/**/
describe('when there are subsections', () => {
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
imports: [NoopAnimationsModule, TranslateModule.forRoot()],
declarations: [ExpandableAdminSidebarSectionComponent, TestComponent],
providers: [
{provide: 'sectionDataProvider', useValue: {icon: iconString, model: {}}},
{provide: MenuService, useValue: menuService},
{provide: CSSVariableService, useClass: CSSVariableServiceStub},
{provide: Router, useValue: new RouterStub()},
]
}).overrideComponent(ExpandableAdminSidebarSectionComponent, {
set: {
entryComponents: [TestComponent]
}
});
})
.compileComponents();
}));
beforeEach(() => {
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);
fixture.detectChanges();
});
it('should call toggleActiveSection on the menuService', () => {
expect(menuService.toggleActiveSection).toHaveBeenCalled();
it('should create', () => {
expect(component).toBeTruthy();
});
it('should set the right icon', () => {
const icon = fixture.debugElement.query(By.css('.shortcut-icon > i.fas'));
expect(icon.nativeElement.getAttribute('class')).toContain('fa-' + iconString);
});
describe('when the header text is clicked', () => {
beforeEach(() => {
spyOn(menuService, 'toggleActiveSection');
const sidebarToggler = fixture.debugElement.query(By.css('.sidebar-section > div.nav-item'));
sidebarToggler.triggerEventHandler('click', {
preventDefault: () => {/**/
}
});
});
it('should call toggleActiveSection on the menuService', () => {
expect(menuService.toggleActiveSection).toHaveBeenCalled();
});
});
});
describe('when there are no subsections', () => {
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
imports: [NoopAnimationsModule, TranslateModule.forRoot()],
declarations: [ExpandableAdminSidebarSectionComponent, TestComponent],
providers: [
{provide: 'sectionDataProvider', useValue: {icon: iconString, model: {}}},
{provide: MenuService, useValue: menuService},
{provide: CSSVariableService, useClass: CSSVariableServiceStub},
{provide: Router, useValue: new RouterStub()},
]
}).overrideComponent(ExpandableAdminSidebarSectionComponent, {
set: {
entryComponents: [TestComponent]
}
})
.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();
});
});
});

View File

@@ -36,7 +36,6 @@ import { ThemedPageErrorComponent } from './page-error/themed-page-error.compone
import { ThemedPageInternalServerErrorComponent } from './page-internal-server-error/themed-page-internal-server-error.component';
import { ThemedPageNotFoundComponent } from './pagenotfound/themed-pagenotfound.component';
import { PROCESS_MODULE_PATH } from './process-page/process-page-routing.paths';
// import { resolveStaticMenus } from './shared/menu/menu.resolver';
@NgModule({
imports: [
@@ -48,8 +47,6 @@ import { PROCESS_MODULE_PATH } from './process-page/process-page-routing.paths';
canActivate: [AuthBlockingGuard],
canActivateChild: [ServerCheckGuard],
resolve: [
// resolveStaticMenus(),
// MenuResolver,
],
children: [
{ path: '', redirectTo: '/home', pathMatch: 'full' },

View File

@@ -4,7 +4,6 @@ import { BrowseByGuard } from './browse-by-guard';
import { BrowseByDSOBreadcrumbResolver } from './browse-by-dso-breadcrumb.resolver';
import { BrowseByI18nBreadcrumbResolver } from './browse-by-i18n-breadcrumb.resolver';
import { ThemedBrowseBySwitcherComponent } from './browse-by-switcher/themed-browse-by-switcher.component';
import { DSOEditMenuResolver } from '../shared/dso-page/dso-edit-menu.resolver';
@NgModule({
imports: [
@@ -13,7 +12,6 @@ import { DSOEditMenuResolver } from '../shared/dso-page/dso-edit-menu.resolver';
path: '',
resolve: {
breadcrumb: BrowseByDSOBreadcrumbResolver,
menu: DSOEditMenuResolver
},
children: [
{

View File

@@ -1,423 +0,0 @@
import { TestBed, waitForAsync } from '@angular/core/testing';
import { MenuResolver } from './menu.resolver';
import { of as observableOf } from 'rxjs';
import { FeatureID } from './core/data/feature-authorization/feature-id';
import { TranslateModule } from '@ngx-translate/core';
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
import { RouterTestingModule } from '@angular/router/testing';
import { AdminSidebarComponent } from './admin/admin-sidebar/admin-sidebar.component';
import { NO_ERRORS_SCHEMA } from '@angular/core';
import { MenuService } from './shared/menu/menu.service';
import { AuthorizationDataService } from './core/data/feature-authorization/authorization-data.service';
import { ScriptDataService } from './core/data/processes/script-data.service';
import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
import { MenuServiceStub } from './shared/testing/menu-service.stub';
import { MenuID } from './shared/menu/menu-id.model';
import { BrowseService } from './core/browse/browse.service';
import { cold } from 'jasmine-marbles';
import createSpy = jasmine.createSpy;
import { createSuccessfulRemoteDataObject$ } from './shared/remote-data.utils';
import { createPaginatedList } from './shared/testing/utils.test';
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: MenuResolver;
let menuService;
let browseService;
let authorizationService;
let scriptService;
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)
});
TestBed.configureTestingModule({
imports: [TranslateModule.forRoot(), NoopAnimationsModule, RouterTestingModule],
declarations: [AdminSidebarComponent],
providers: [
{ provide: MenuService, useValue: menuService },
{ provide: BrowseService, useValue: browseService },
{ provide: AuthorizationDataService, useValue: authorizationService },
{ provide: ScriptDataService, useValue: scriptService },
{
provide: NgbModal, useValue: {
open: () => {/*comment*/
}
}
}
],
schemas: [NO_ERRORS_SCHEMA]
});
resolver = TestBed.inject(MenuResolver);
}));
it('should be created', () => {
expect(resolver).toBeTruthy();
});
describe('resolve', () => {
it('should create all menus', (done) => {
spyOn(resolver, 'createPublicMenu$').and.returnValue(observableOf(true));
spyOn(resolver, 'createAdminMenu$').and.returnValue(observableOf(true));
resolver.resolve(null, null).subscribe(resolved => {
expect(resolved).toBeTrue();
expect(resolver.createPublicMenu$).toHaveBeenCalled();
expect(resolver.createAdminMenu$).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, 'createAdminMenu$').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,
}));
});
});
});
});

View File

@@ -1,703 +0,0 @@
import { Injectable } from '@angular/core';
import { ActivatedRouteSnapshot, Resolve, RouterStateSnapshot } from '@angular/router';
import { combineLatest as observableCombineLatest, combineLatest, Observable } from 'rxjs';
import { MenuID } from './shared/menu/menu-id.model';
import { MenuState } from './shared/menu/menu-state.model';
import { MenuItemType } from './shared/menu/menu-item-type.model';
import { LinkMenuItemModel } from './shared/menu/menu-item/models/link.model';
import { getFirstCompletedRemoteData } from './core/shared/operators';
import { PaginatedList } from './core/data/paginated-list.model';
import { BrowseDefinition } from './core/shared/browse-definition.model';
import { RemoteData } from './core/data/remote-data';
import { TextMenuItemModel } from './shared/menu/menu-item/models/text.model';
import { BrowseService } from './core/browse/browse.service';
import { MenuService } from './shared/menu/menu.service';
import { filter, find, map, take } from 'rxjs/operators';
import { hasValue } from './shared/empty.util';
import { FeatureID } from './core/data/feature-authorization/feature-id';
import {
ThemedCreateCommunityParentSelectorComponent
} from './shared/dso-selector/modal-wrappers/create-community-parent-selector/themed-create-community-parent-selector.component';
import { OnClickMenuItemModel } from './shared/menu/menu-item/models/onclick.model';
import {
ThemedCreateCollectionParentSelectorComponent
} from './shared/dso-selector/modal-wrappers/create-collection-parent-selector/themed-create-collection-parent-selector.component';
import {
ThemedCreateItemParentSelectorComponent
} from './shared/dso-selector/modal-wrappers/create-item-parent-selector/themed-create-item-parent-selector.component';
import {
ThemedEditCommunitySelectorComponent
} from './shared/dso-selector/modal-wrappers/edit-community-selector/themed-edit-community-selector.component';
import {
ThemedEditCollectionSelectorComponent
} from './shared/dso-selector/modal-wrappers/edit-collection-selector/themed-edit-collection-selector.component';
import {
ThemedEditItemSelectorComponent
} from './shared/dso-selector/modal-wrappers/edit-item-selector/themed-edit-item-selector.component';
import {
ExportMetadataSelectorComponent
} from './shared/dso-selector/modal-wrappers/export-metadata-selector/export-metadata-selector.component';
import { AuthorizationDataService } from './core/data/feature-authorization/authorization-data.service';
import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
import {
METADATA_EXPORT_SCRIPT_NAME,
METADATA_IMPORT_SCRIPT_NAME,
ScriptDataService
} from './core/data/processes/script-data.service';
import {
ExportBatchSelectorComponent
} from './shared/dso-selector/modal-wrappers/export-batch-selector/export-batch-selector.component';
/**
* Creates all of the app's menus
*/
@Injectable({
providedIn: 'root'
})
export class MenuResolver implements Resolve<boolean> {
constructor(
protected menuService: MenuService,
protected browseService: BrowseService,
protected authorizationService: AuthorizationDataService,
protected modalService: NgbModal,
protected scriptDataService: ScriptDataService,
) {
}
/**
* Initialize all menus
*/
resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean> {
return combineLatest([
this.createPublicMenu$(),
this.createAdminMenu$(),
]).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,
}
);
}
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}
*/
createAdminMenu$() {
this.createMainMenuSections();
this.createSiteAdministratorMenuSections();
this.createExportMenuSections();
this.createImportMenuSections();
this.createAccessControlMenuSections();
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),
]).subscribe(([isCollectionAdmin, isCommunityAdmin, isSiteAdmin, canSubmit, canEditItem]) => {
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,
},
];
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
},
{
id: 'health',
active: false,
visible: isSiteAdmin,
model: {
type: MenuItemType.LINK,
text: 'menu.section.health',
link: '/health'
} as LinkMenuItemModel,
icon: 'heartbeat',
index: 11
},
];
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 = [
/* Admin Search */
{
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,
})));
});
}
}

View File

@@ -10,6 +10,7 @@ import { MenuService } from '../../shared/menu/menu.service';
import { HostWindowServiceStub } from '../../shared/testing/host-window-service.stub';
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
import { VarDirective } from '../../shared/utils/var.directive';
import { MenuItemModels } from '../../shared/menu/menu-section.model';
describe('ExpandableNavbarSectionComponent', () => {
let component: ExpandableNavbarSectionComponent;
@@ -35,7 +36,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;
@@ -184,7 +185,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;
@@ -195,6 +196,7 @@ describe('ExpandableNavbarSectionComponent', () => {
describe('when the mouse enters the section header', () => {
beforeEach(() => {
spyOn(menuService, 'activateSection');
console.log(fixture.nativeElement.innerHTML);
const sidebarToggler = fixture.debugElement.query(By.css('div.nav-item.dropdown > a'));
sidebarToggler.triggerEventHandler('mouseenter', {
preventDefault: () => {/**/

View File

@@ -1,259 +0,0 @@
import { TestBed, waitForAsync } from '@angular/core/testing';
import { MenuServiceStub } from '../testing/menu-service.stub';
import { of as observableOf } from 'rxjs';
import { TranslateModule, TranslateService } from '@ngx-translate/core';
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
import { RouterTestingModule } from '@angular/router/testing';
import { AdminSidebarComponent } from '../../admin/admin-sidebar/admin-sidebar.component';
import { MenuService } from '../menu/menu.service';
import { AuthorizationDataService } from '../../core/data/feature-authorization/authorization-data.service';
import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
import { NO_ERRORS_SCHEMA } from '@angular/core';
import { DSOEditMenuResolver } from './dso-edit-menu.resolver';
import { DsoVersioningModalService } from './dso-versioning-modal-service/dso-versioning-modal.service';
import { DSpaceObjectDataService } from '../../core/data/dspace-object-data.service';
import { Item } from '../../core/shared/item.model';
import { createFailedRemoteDataObject$, createSuccessfulRemoteDataObject$ } from '../remote-data.utils';
import { MenuID } from '../menu/menu-id.model';
import { MenuItemType } from '../menu/menu-item-type.model';
import { TextMenuItemModel } from '../menu/menu-item/models/text.model';
import { LinkMenuItemModel } from '../menu/menu-item/models/link.model';
import { ResearcherProfileDataService } from '../../core/profile/researcher-profile-data.service';
import { NotificationsService } from '../notifications/notifications.service';
describe('DSOEditMenuResolver', () => {
const MENU_STATE = {
id: 'some menu'
};
let resolver: DSOEditMenuResolver;
let dSpaceObjectDataService;
let menuService;
let authorizationService;
let dsoVersioningModalService;
let researcherProfileService;
let notificationsService;
let translate;
const route = {
data: {
menu: {
'statistics': [{
id: 'statistics-dummy-1',
active: false,
visible: true,
model: null
}]
}
},
params: {id: 'test-uuid'},
};
const state = {
url: 'test-url'
};
const testObject = Object.assign(new Item(), {uuid: 'test-uuid', type: 'item', _links: {self: {href: 'self-link'}}});
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(() => {
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: {},
});
TestBed.configureTestingModule({
imports: [TranslateModule.forRoot(), NoopAnimationsModule, RouterTestingModule],
declarations: [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: NgbModal, useValue: {
open: () => {/*comment*/
}
}
}
],
schemas: [NO_ERRORS_SCHEMA]
});
resolver = TestBed.inject(DSOEditMenuResolver);
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-uuid'})),
...dummySections2.map((menu) => Object.assign(menu, {id: menu.id + '-test-uuid'}))
]
}
);
expect(dSpaceObjectDataService.findById).toHaveBeenCalledWith('test-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', () => {
it('should return as first part the item version, orcid and claim list ', (done) => {
const result = resolver.getDsoMenus(testObject, route, state);
result[0].subscribe((menuList) => {
expect(menuList.length).toEqual(3);
expect(menuList[0].id).toEqual('orcid-dso');
expect(menuList[0].active).toEqual(false);
// Visible should be false due to the item not being of type person
expect(menuList[0].visible).toEqual(false);
expect(menuList[0].model.type).toEqual(MenuItemType.LINK);
expect(menuList[1].id).toEqual('version-dso');
expect(menuList[1].active).toEqual(false);
expect(menuList[1].visible).toEqual(true);
expect(menuList[1].model.type).toEqual(MenuItemType.ONCLICK);
expect((menuList[1].model as TextMenuItemModel).text).toEqual('message');
expect(menuList[1].model.disabled).toEqual(false);
expect(menuList[1].icon).toEqual('code-branch');
expect(menuList[2].id).toEqual('claim-dso');
expect(menuList[2].active).toEqual(false);
// Visible should be false due to the item not being of type person
expect(menuList[2].visible).toEqual(false);
expect(menuList[2].model.type).toEqual(MenuItemType.ONCLICK);
expect((menuList[2].model as TextMenuItemModel).text).toEqual('item.page.claim.button');
done();
});
});
it('should return as second part the common list ', (done) => {
const result = resolver.getDsoMenus(testObject, route, state);
result[1].subscribe((menuList) => {
expect(menuList.length).toEqual(1);
expect(menuList[0].id).toEqual('edit-dso');
expect(menuList[0].active).toEqual(false);
expect(menuList[0].visible).toEqual(true);
expect(menuList[0].model.type).toEqual(MenuItemType.LINK);
expect((menuList[0].model as LinkMenuItemModel).text).toEqual('item.page.edit');
expect((menuList[0].model as LinkMenuItemModel).link).toEqual('/items/test-uuid/edit/metadata');
expect(menuList[0].icon).toEqual('pencil-alt');
done();
});
});
});
});

View File

@@ -1,229 +0,0 @@
import { ActivatedRouteSnapshot, Resolve, RouterStateSnapshot } from '@angular/router';
import { combineLatest, Observable, of as observableOf } from 'rxjs';
import { FeatureID } from '../../core/data/feature-authorization/feature-id';
import { MenuService } from '../menu/menu.service';
import { AuthorizationDataService } from '../../core/data/feature-authorization/authorization-data.service';
import { Injectable } from '@angular/core';
import { LinkMenuItemModel } from '../menu/menu-item/models/link.model';
import { Item } from '../../core/shared/item.model';
import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
import { OnClickMenuItemModel } from '../menu/menu-item/models/onclick.model';
import { getFirstCompletedRemoteData } from '../../core/shared/operators';
import { map, switchMap } from 'rxjs/operators';
import { DSpaceObjectDataService } from '../../core/data/dspace-object-data.service';
import { URLCombiner } from '../../core/url-combiner/url-combiner';
import { DsoVersioningModalService } from './dso-versioning-modal-service/dso-versioning-modal.service';
import { hasNoValue, hasValue, isNotEmpty } from '../empty.util';
import { MenuID } from '../menu/menu-id.model';
import { MenuItemType } from '../menu/menu-item-type.model';
import { MenuSection } from '../menu/menu-section.model';
import { getDSORoute } from '../../app-routing-paths';
import { ResearcherProfileDataService } from '../../core/profile/researcher-profile-data.service';
import { NotificationsService } from '../notifications/notifications.service';
import { TranslateService } from '@ngx-translate/core';
/**
* Creates the menus for the dspace object pages
*/
@Injectable({
providedIn: 'root'
})
export class DSOEditMenuResolver implements Resolve<{ [key: string]: MenuSection[] }> {
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,
) {
}
/**
* 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.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),
]).pipe(
map(([canCreateVersion, disableVersioning, versionTooltip, canSynchronizeWithOrcid, canClaimItem]) => {
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
},
];
}),
);
} 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;
});
}
}

View File

@@ -11,6 +11,7 @@ import { Component } from '@angular/core';
import { DsoEditMenuExpandableSectionComponent } from './dso-edit-menu-expandable-section.component';
import { By } from '@angular/platform-browser';
import { MenuItemType } from 'src/app/shared/menu/menu-item-type.model';
import { MenuItemModels } from '../../../menu/menu-section.model';
describe('DsoEditMenuExpandableSectionComponent', () => {
let component: DsoEditMenuExpandableSectionComponent;
@@ -30,39 +31,82 @@ describe('DsoEditMenuExpandableSectionComponent', () => {
icon: iconString
};
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
imports: [TranslateModule.forRoot()],
declarations: [DsoEditMenuExpandableSectionComponent, TestComponent],
providers: [
{provide: 'sectionDataProvider', useValue: dummySection},
{provide: MenuService, useValue: menuService},
{provide: CSSVariableService, useClass: CSSVariableServiceStub},
{provide: Router, useValue: new RouterStub()},
]
}).overrideComponent(DsoEditMenuExpandableSectionComponent, {
set: {
entryComponents: [TestComponent]
}
})
.compileComponents();
}));
describe('when there are subsections', () => {
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
imports: [TranslateModule.forRoot()],
declarations: [DsoEditMenuExpandableSectionComponent, TestComponent],
providers: [
{provide: 'sectionDataProvider', useValue: dummySection},
{provide: MenuService, useValue: menuService},
{provide: CSSVariableService, useClass: CSSVariableServiceStub},
{provide: Router, useValue: new RouterStub()},
]
}).overrideComponent(DsoEditMenuExpandableSectionComponent, {
set: {
entryComponents: [TestComponent]
}
})
.compileComponents();
}));
beforeEach(() => {
spyOn(menuService, 'getSubSectionsByParentID').and.returnValue(observableOf([]));
fixture = TestBed.createComponent(DsoEditMenuExpandableSectionComponent);
component = fixture.componentInstance;
spyOn(component as any, 'getMenuItemComponent').and.returnValue(TestComponent);
fixture.detectChanges();
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);
});
});
it('should create', () => {
expect(component).toBeTruthy();
});
describe('when there are no subsections', () => {
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
imports: [TranslateModule.forRoot()],
declarations: [DsoEditMenuExpandableSectionComponent, TestComponent],
providers: [
{provide: 'sectionDataProvider', useValue: dummySection},
{provide: MenuService, useValue: menuService},
{provide: CSSVariableService, useClass: CSSVariableServiceStub},
{provide: Router, useValue: new RouterStub()},
]
}).overrideComponent(DsoEditMenuExpandableSectionComponent, {
set: {
entryComponents: [TestComponent]
}
})
.compileComponents();
}));
it('should show a button with the icon', () => {
const button = fixture.debugElement.query(By.css('.btn-dark'));
expect(button.nativeElement.innerHTML).toContain('fa-' + iconString);
beforeEach(() => {
spyOn(menuService, 'getSubSectionsByParentID').and.returnValue(observableOf([]));
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 now show a button', () => {
const button = fixture.debugElement.query(By.css('.btn-dark'));
expect(button).toBeNull();
});
});
});

View File

@@ -10,7 +10,6 @@ import { AuthorizationDataService } from '../../../core/data/feature-authorizati
import { AuthService } from '../../../core/auth/auth.service';
import { AuthServiceStub } from '../../testing/auth-service.stub';
import { MenuService } from '../../menu/menu.service';
import { MenuItemModel } from '../../menu/menu-item/models/menu-item.model';
import { ThemeService } from '../../theme-support/theme.service';
import { getMockThemeService } from '../../mocks/theme-service.mock';

View File

@@ -1,8 +0,0 @@
<button *ngIf="isAuthorized$ | async" data-test="subscription-button"
(click)="openSubscriptionModal()"
[ngbTooltip]="'subscriptions.tooltip' | translate"
[title]="'subscriptions.tooltip' | translate"
[attr.aria-label]="'subscriptions.tooltip' | translate"
class="subscription-button btn btn-dark btn-sm">
<i class="fas fa-bell fa-fw"></i>
</button>

View File

@@ -1,83 +0,0 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { DsoPageSubscriptionButtonComponent } from './dso-page-subscription-button.component';
import { AuthorizationDataService } from '../../../core/data/feature-authorization/authorization-data.service';
import { of as observableOf } from 'rxjs';
import { NgbModalModule } from '@ng-bootstrap/ng-bootstrap';
import { By } from '@angular/platform-browser';
import { DebugElement } from '@angular/core';
import { Item } from '../../../core/shared/item.model';
import { ITEM } from '../../../core/shared/item.resource-type';
import { TranslateLoader, TranslateModule } from '@ngx-translate/core';
import { TranslateLoaderMock } from '../../mocks/translate-loader.mock';
describe('DsoPageSubscriptionButtonComponent', () => {
let component: DsoPageSubscriptionButtonComponent;
let fixture: ComponentFixture<DsoPageSubscriptionButtonComponent>;
let de: DebugElement;
const authorizationService = jasmine.createSpyObj('authorizationService', {
isAuthorized: jasmine.createSpy('isAuthorized') // observableOf(true)
});
const mockItem = Object.assign(new Item(), {
id: 'fake-id',
uuid: 'fake-id',
handle: 'fake/handle',
lastModified: '2018',
type: ITEM,
_links: {
self: {
href: 'https://localhost:8000/items/fake-id'
}
}
});
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [
NgbModalModule,
TranslateModule.forRoot({
loader: {
provide: TranslateLoader,
useClass: TranslateLoaderMock
}
})
],
declarations: [ DsoPageSubscriptionButtonComponent ],
providers: [
{ provide: AuthorizationDataService, useValue: authorizationService },
]
})
.compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(DsoPageSubscriptionButtonComponent);
component = fixture.componentInstance;
de = fixture.debugElement;
component.dso = mockItem;
});
describe('when is authorized', () => {
beforeEach(() => {
authorizationService.isAuthorized.and.returnValue(observableOf(true));
fixture.detectChanges();
});
it('should display subscription button', () => {
expect(de.query(By.css(' [data-test="subscription-button"]'))).toBeTruthy();
});
});
describe('when is not authorized', () => {
beforeEach(() => {
authorizationService.isAuthorized.and.returnValue(observableOf(false));
fixture.detectChanges();
});
it('should not display subscription button', () => {
expect(de.query(By.css(' [data-test="subscription-button"]'))).toBeNull();
});
});
});

View File

@@ -1,57 +0,0 @@
import { Component, Input, OnInit } from '@angular/core';
import { Observable, of } from 'rxjs';
import { NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap';
import { DSpaceObject } from '../../../core/shared/dspace-object.model';
import { AuthorizationDataService } from '../../../core/data/feature-authorization/authorization-data.service';
import { SubscriptionModalComponent } from '../../subscriptions/subscription-modal/subscription-modal.component';
import { FeatureID } from '../../../core/data/feature-authorization/feature-id';
@Component({
selector: 'ds-dso-page-subscription-button',
templateUrl: './dso-page-subscription-button.component.html',
styleUrls: ['./dso-page-subscription-button.component.scss']
})
/**
* Display a button that opens the modal to manage subscriptions
*/
export class DsoPageSubscriptionButtonComponent implements OnInit {
/**
* Whether the current user is authorized to edit the DSpaceObject
*/
isAuthorized$: Observable<boolean> = of(false);
/**
* Reference to NgbModal
*/
public modalRef: NgbModalRef;
/**
* DSpaceObject that is being viewed
*/
@Input() dso: DSpaceObject;
constructor(
protected authorizationService: AuthorizationDataService,
private modalService: NgbModal,
) {
}
/**
* check if the current DSpaceObject can be subscribed by the user
*/
ngOnInit(): void {
this.isAuthorized$ = this.authorizationService.isAuthorized(FeatureID.CanSubscribe, this.dso.self);
}
/**
* Open the modal to subscribe to the related DSpaceObject
*/
public openSubscriptionModal() {
this.modalRef = this.modalService.open(SubscriptionModalComponent);
this.modalRef.componentInstance.dso = this.dso;
}
}

View File

@@ -7,13 +7,15 @@
*/
/* eslint-disable max-classes-per-file */
import { ActivatedRouteSnapshot, RouterStateSnapshot, } from '@angular/router';
import flatten from 'lodash/flatten';
import { combineLatest, Observable, } from 'rxjs';
import { map } from 'rxjs/operators';
import { Observable, } from 'rxjs';
import { MenuID } from './menu-id.model';
import { MenuItemModels } from './menu-section.model';
import { Type } from '@angular/core';
/**
* Partial menu section
* This object acts like a menu section but with certain properties being optional
*/
export interface PartialMenuSection {
id?: string;
visible: boolean;
@@ -26,15 +28,28 @@ export interface PartialMenuSection {
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?: string[];
@@ -42,6 +57,9 @@ export class MenuProviderTypeWithOptions {
}
/**
* Abstract class to be extended when creating menu providers
*/
export abstract class AbstractMenuProvider implements MenuProvider {
/**
@@ -54,17 +72,44 @@ export abstract class AbstractMenuProvider implements MenuProvider {
* 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?: string[];
/**
* 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;
/**
* Whether the menu section or top section of this provider will always be rendered as expandable and hidden when no children are present
* 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;
/**
* 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: string[]): MenuProviderTypeWithOptions {
if (!AbstractMenuProvider.isPrototypeOf(this)) {
throw new Error(
@@ -77,7 +122,7 @@ export abstract class AbstractMenuProvider implements MenuProvider {
}
/**
* Method to add sub menu providers to this top provider
* 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 {
@@ -91,13 +136,13 @@ export abstract class AbstractMenuProvider implements MenuProvider {
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 concat(...sections$: Observable<PartialMenuSection[]>[]): Observable<PartialMenuSection[]> {
return combineLatest(sections$).pipe(
map(sections => flatten(sections)),
);
}
}

View File

@@ -0,0 +1,165 @@
import { AbstractMenuProvider, PartialMenuSection } from './menu-provider.model';
import { MenuID } from './menu-id.model';
import { ActivatedRouteSnapshot, ResolveEnd, RouterStateSnapshot, UrlSegment } from '@angular/router';
import { of as observableOf } from 'rxjs';
import { MenuItemType } from './menu-item-type.model';
import { waitForAsync } from '@angular/core/testing';
import { MenuProviderService } from './menu-provider.service';
import { MenuService } from './menu.service';
import { COMMUNITY_MODULE_PATH } from '../../community-page/community-page-routing-paths';
import { COLLECTION_MODULE_PATH } from '../../collection-page/collection-page-routing-paths';
describe('MenuProviderService', () => {
class TestMenuProvider extends AbstractMenuProvider {
constructor(
public menuID: MenuID,
public shouldPersistOnRouteChange: boolean,
public menuProviderId: string,
public index: number,
public activePaths: string[],
public parentID: string,
public alwaysRenderExpandable: boolean,
public sections: PartialMenuSection[]
) {
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', {})]}} 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]);
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, 'provider4', 3, [COMMUNITY_MODULE_PATH, COLLECTION_MODULE_PATH], undefined, false, [section]);
const listOfProvider = [persistentProvider1, persistentProvider2, nonPersistentProvider3, nonPersistentProvider4, nonPersistentProvider5WithRoutes];
const expectedSection1 = generateAddedSection(persistentProvider1, section);
const expectedSection2 = generateAddedSection(persistentProvider2, section);
const expectedSection3 = generateAddedSection(nonPersistentProvider3, section);
const expectedSection4 = generateAddedSection(nonPersistentProvider4, section);
const expectedSection5 = generateAddedSection(nonPersistentProvider5WithRoutes, section);
function generateAddedSection(provider, sectionToAdd) {
return {
...sectionToAdd,
id: sectionToAdd.id ?? `${provider.menuProviderId}`,
parentID: sectionToAdd.parentID ?? provider.parentID,
index: sectionToAdd.index ?? provider.index,
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', () => {
it('should initialise the menu sections from the persistent providers', () => {
menuProviderService.initPersistentMenus();
expect(menuService.addSection).toHaveBeenCalledWith(persistentProvider1.menuID, expectedSection1);
expect(menuService.addSection).toHaveBeenCalledWith(persistentProvider2.menuID, expectedSection2);
expect(menuService.addSection).not.toHaveBeenCalledWith(nonPersistentProvider3.menuID, expectedSection3);
expect(menuService.addSection).not.toHaveBeenCalledWith(nonPersistentProvider4.menuID, expectedSection4);
expect(menuService.addSection).not.toHaveBeenCalledWith(nonPersistentProvider5WithRoutes.menuID, expectedSection5);
});
});
describe('resolveRouteMenus with no matching path specific providers', () => {
it('should remove the current non persistent menus and add the general non persistent menus', () => {
const route = {};
const state = {url: 'test-url'};
menuProviderService.resolveRouteMenus(route as any, state as any).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, expectedSection2);
expect(menuService.addSection).toHaveBeenCalledWith(nonPersistentProvider3.menuID, expectedSection3);
expect(menuService.addSection).toHaveBeenCalledWith(nonPersistentProvider4.menuID, expectedSection4);
expect(menuService.addSection).not.toHaveBeenCalledWith(nonPersistentProvider5WithRoutes.menuID, expectedSection5);
});
});
describe('resolveRouteMenus with a matching path specific provider', () => {
it('should remove the current non persistent menus and add the general non persistent menus', () => {
const route = {};
const state = {url: `xxxx/${COMMUNITY_MODULE_PATH}/xxxxxx`};
menuProviderService.resolveRouteMenus(route as any, state as any).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, expectedSection2);
expect(menuService.addSection).toHaveBeenCalledWith(nonPersistentProvider3.menuID, expectedSection3);
expect(menuService.addSection).toHaveBeenCalledWith(nonPersistentProvider4.menuID, expectedSection4);
expect(menuService.addSection).toHaveBeenCalledWith(nonPersistentProvider5WithRoutes.menuID, expectedSection5);
});
});
describe('listenForRouteChanges ', () => {
it('should listen to the route changes and update the menu sections based on the retrieved state and route', () => {
menuProviderService.listenForRouteChanges();
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, expectedSection2);
expect(menuService.addSection).toHaveBeenCalledWith(nonPersistentProvider3.menuID, expectedSection3);
expect(menuService.addSection).toHaveBeenCalledWith(nonPersistentProvider4.menuID, expectedSection4);
expect(menuService.addSection).not.toHaveBeenCalledWith(nonPersistentProvider5WithRoutes.menuID, expectedSection5);
});
});
});

View File

@@ -6,17 +6,21 @@
* http://www.dspace.org/license/
*/
import { Inject, Injectable, Injector, Optional, } from '@angular/core';
import { ActivatedRoute, ActivatedRouteSnapshot, ResolveEnd, Router, RouterStateSnapshot, } from '@angular/router';
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 { MenuID } from './menu-id.model';
import { AbstractMenuProvider, PartialMenuSection } from './menu-provider';
import { AbstractMenuProvider, PartialMenuSection } from './menu-provider.model';
import { MenuState } from './menu-state.model';
import { MenuService } from './menu.service';
import { MENU_PROVIDER } from './menu.structure';
/**
* 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',
})
@@ -24,9 +28,7 @@ export class MenuProviderService {
constructor(
@Inject(MENU_PROVIDER) @Optional() protected providers: ReadonlyArray<AbstractMenuProvider>,
protected menuService: MenuService,
protected injector: Injector,
protected router: Router,
protected route: ActivatedRoute,
) {
}
@@ -42,22 +44,22 @@ export class MenuProviderService {
);
}
/**
* Listen for route changes and resolve the route dependent menu sections on route change
*/
listenForRouteChanges() {
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);
}),
).subscribe((done) => {
Object.values(MenuID).forEach((menuID) => {
this.menuService.buildRouteMenuSections(menuID);
});
});
).subscribe();
}
/**
* Get the full current route
*/
private getCurrentRoute(route: ActivatedRouteSnapshot): ActivatedRouteSnapshot {
while (route.firstChild) {
route = route.firstChild;
@@ -66,6 +68,9 @@ export class MenuProviderService {
}
/**
* Initialise the persistent menu sections
*/
public initPersistentMenus() {
combineLatest([
...this.providers
@@ -87,20 +92,22 @@ export class MenuProviderService {
sections: PartialMenuSection[]
}, sectionIndex) => {
providerWithSection.sections.forEach((section) => {
this.addSection(providerWithSection, section);
this.addSection(providerWithSection.provider, section);
});
return this.waitForMenu$(providerWithSection.provider.menuID);
});
return [waitForMenus];
}),
map(done => done.every(Boolean)),
).subscribe((done) => {
Object.values(MenuID).forEach((menuID) => {
this.menuService.buildRouteMenuSections(menuID);
});
});
take(1),
).subscribe();
}
/**
* Resolve the non-persistent route based menu sections
* @param route - the current route
* @param state - the current router state
*/
public resolveRouteMenus(
route: ActivatedRouteSnapshot,
state: RouterStateSnapshot
@@ -149,7 +156,7 @@ export class MenuProviderService {
sections: PartialMenuSection[]
}) => {
providerWithSection.sections.forEach((section) => {
this.addSection(providerWithSection, section);
this.addSection(providerWithSection.provider, section);
});
return this.waitForMenu$(providerWithSection.provider.menuID);
});
@@ -159,22 +166,28 @@ export class MenuProviderService {
);
}
private addSection(providerWithSection: {
provider: AbstractMenuProvider;
sections: PartialMenuSection[]
}, section: PartialMenuSection) {
this.menuService.addSection(providerWithSection.provider.menuID, {
/**
* 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) {
this.menuService.addSection(provider.menuID, {
...section,
id: section.id ?? `${providerWithSection.provider.menuProviderId}`,
parentID: section.parentID ?? providerWithSection.provider.parentID,
index: section.index ?? providerWithSection.provider.index,
shouldPersistOnRouteChange: section.shouldPersistOnRouteChange ?? providerWithSection.provider.shouldPersistOnRouteChange,
alwaysRenderExpandable: section.alwaysRenderExpandable ?? providerWithSection.provider.alwaysRenderExpandable,
id: section.id ?? `${provider.menuProviderId}`,
parentID: section.parentID ?? provider.parentID,
index: section.index ?? provider.index,
shouldPersistOnRouteChange: section.shouldPersistOnRouteChange ?? provider.shouldPersistOnRouteChange,
alwaysRenderExpandable: section.alwaysRenderExpandable ?? provider.alwaysRenderExpandable,
});
}
private removeNonPersistentSections(menuSectionsPerMenu) {
menuSectionsPerMenu.forEach((menu) => {
/**
* 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);
});

View File

@@ -1,4 +1,3 @@
import { MenuItemType } from './menu-item-type.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';
@@ -14,26 +13,6 @@ export type MenuItemModels =
| SearchMenuItemModel
| TextMenuItemModel;
function itemModelFactory(type: MenuItemType): MenuItemModels {
switch (type) {
case MenuItemType.TEXT:
return new TextMenuItemModel();
case MenuItemType.LINK:
return new LinkMenuItemModel();
case MenuItemType.ALTMETRIC:
return new AltmetricMenuItemModel();
case MenuItemType.SEARCH:
return new SearchMenuItemModel();
case MenuItemType.ONCLICK:
return new OnClickMenuItemModel();
case MenuItemType.EXTERNAL:
return new ExternalLinkMenuItemModel();
default: {
throw new Error(`No such menu item type: ${type}`);
}
}
}
export interface MenuSection {
/**
* The identifier for this section
@@ -80,5 +59,9 @@ export interface MenuSection {
*/
icon?: string;
/**
* 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;
}

View File

@@ -32,7 +32,7 @@ describe('MenuComponent', () => {
} as TextMenuItemModel,
icon: 'globe',
visible: true,
}
};
const mockMenuID = 'mock-menuID' as MenuID;

View File

@@ -1,19 +0,0 @@
/**
* 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/
*/
// export function resolveStaticMenus(): (ActivatedRouteSnapshot, RouterStateSnapshot, ProviderMenuService) => Observable<boolean> {
// return (
// route: ActivatedRouteSnapshot,
// state: RouterStateSnapshot,
// menuProviderService: MenuProviderService = inject(MenuProviderService),
// ) => menuProviderService.resolveStaticMenu();
// }

View File

@@ -39,9 +39,7 @@ describe('MenuService', () => {
let topSections;
let initialState;
let routeDataMenuSection: MenuSection;
let routeDataMenuSectionResolved: MenuSection;
let routeDataMenuChildSection: MenuSection;
let toBeRemovedMenuSection: MenuSection;
let alreadyPresentMenuSection: MenuSection;
let route;
let router;
@@ -106,16 +104,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',
@@ -127,16 +115,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,
@@ -539,69 +517,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', () => {
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).toHaveBeenCalledWith(MenuID.PUBLIC, routeDataMenuChildSection);
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);
});
});
});

View File

@@ -2,7 +2,7 @@ import { Injectable } from '@angular/core';
import { createSelector, MemoizedSelector, select, Store } from '@ngrx/store';
import { AppState, keySelector } from '../../app.reducer';
import { combineLatest as observableCombineLatest, Observable } from 'rxjs';
import { filter, map, switchMap, take } from 'rxjs/operators';
import { map, switchMap } from 'rxjs/operators';
import {
ActivateMenuSectionAction,
AddMenuSectionAction,
@@ -17,12 +17,12 @@ import {
ToggleActiveMenuSectionAction,
ToggleMenuAction,
} from './menu.actions';
import { hasNoValue, hasValue, hasValueOperator, isNotEmpty, isEmpty } from '../empty.util';
import { hasNoValue, hasValue, hasValueOperator, isNotEmpty } from '../empty.util';
import { MenuState } from './menu-state.model';
import { MenuSections } from './menu-sections.model';
import { MenuSection } from './menu-section.model';
import { MenuID } from './menu-id.model';
import { ActivatedRoute, NavigationEnd, Router } from '@angular/router';
import { ActivatedRoute, Router } from '@angular/router';
export function menuKeySelector<T>(key: string, selector): MemoizedSelector<MenuState, T> {
return createSelector(selector, (state) => {
@@ -344,95 +344,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) {
return [...menuSections, ...this.resolveRouteMenuSections(route.firstChild, menuID)];
} 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;
}
}

View File

@@ -0,0 +1,102 @@
import { MenuID } from './menu-id.model';
import { CommunityListMenuProvider } from './providers/community-list.menu';
import { NewMenuProvider } from './providers/new.menu';
import { DsoOptionMenu } from './providers/dso-option.menu';
import { SubscribeMenuProvider } from './providers/comcol-subscribe.menu';
import { COMMUNITY_MODULE_PATH } from '../../community-page/community-page-routing-paths';
import { COLLECTION_MODULE_PATH } from '../../collection-page/collection-page-routing-paths';
import { buildMenuStructure } from './menu.structure';
import { MenuProviderService } from './menu-provider.service';
import { BrowseMenuProvider } from './providers/browse.menu';
import { StatisticsMenuProvider } from './providers/statistics.menu';
import { EditMenuProvider } from './providers/edit.menu';
import { ImportMenuProvider } from './providers/import.menu';
import { ExportMenuProvider } from './providers/export.menu';
import { AccessControlMenuProvider } from './providers/access-control.menu';
import { AdminSearchMenuProvider } from './providers/admin-search.menu';
import { RegistriesMenuProvider } from './providers/registries.menu';
import { CurationMenuProvider } from './providers/curation.menu';
import { ProcessesMenuProvider } from './providers/processes.menu';
import { WorkflowMenuProvider } from './providers/workflow.menu';
import { HealthMenuProvider } from './providers/health.menu';
import { SystemWideAlertMenuProvider } from './providers/system-wide-alert.menu';
import { DSpaceObjectEditMenuProvider } from './providers/dso-edit.menu';
import { ENTITY_MODULE_PATH, ITEM_MODULE_PATH } from '../../item-page/item-page-routing-paths';
import { VersioningMenuProvider } from './providers/item-versioning.menu';
import { OrcidMenuProvider } from './providers/item-orcid.menu';
import { ClaimMenuProvider } from './providers/item-claim.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]: [
DsoOptionMenu.withSubs([
SubscribeMenuProvider.onRoute(COMMUNITY_MODULE_PATH, COLLECTION_MODULE_PATH),
DSpaceObjectEditMenuProvider.onRoute(COMMUNITY_MODULE_PATH, COLLECTION_MODULE_PATH, ITEM_MODULE_PATH, ENTITY_MODULE_PATH),
VersioningMenuProvider.onRoute(ITEM_MODULE_PATH, ENTITY_MODULE_PATH),
OrcidMenuProvider.onRoute(ITEM_MODULE_PATH, ENTITY_MODULE_PATH),
ClaimMenuProvider.onRoute(ITEM_MODULE_PATH, ENTITY_MODULE_PATH, COLLECTION_MODULE_PATH),
]),
],
};
const orderedProviderTypeList =
[
CommunityListMenuProvider,
BrowseMenuProvider,
StatisticsMenuProvider,
NewMenuProvider,
EditMenuProvider,
ImportMenuProvider,
ExportMenuProvider,
AccessControlMenuProvider,
AdminSearchMenuProvider,
RegistriesMenuProvider,
CurationMenuProvider,
ProcessesMenuProvider,
WorkflowMenuProvider,
HealthMenuProvider,
SystemWideAlertMenuProvider,
SubscribeMenuProvider,
DSpaceObjectEditMenuProvider,
VersioningMenuProvider,
OrcidMenuProvider,
ClaimMenuProvider,
DsoOptionMenu,
];
it('should have a double amount of objects with an additional service after the processing', () => {
const result = buildMenuStructure(providerStructure);
expect(result.length).toEqual(orderedProviderTypeList.length * 2 + 1);
});
it('should return a list with the MenuProviderService and then a resolved provider and provider type for each provider in the provided structure', () => {
const result = buildMenuStructure(providerStructure);
expect(result[0]).toEqual(MenuProviderService);
orderedProviderTypeList.forEach((provider, index) => {
expect((result[(index + 1) * 2 - 1] as any).deps).toEqual([provider]);
expect(result[(index + 1) * 2]).toEqual(provider);
});
});
});

View File

@@ -7,7 +7,7 @@
*/
import { InjectionToken, Provider, Type, } from '@angular/core';
import { MenuID } from './menu-id.model';
import { AbstractMenuProvider, MenuProviderTypeWithOptions } from './menu-provider';
import { AbstractMenuProvider, MenuProviderTypeWithOptions } from './menu-provider.model';
import { MenuProviderService } from './menu-provider.service';
import { hasValue, isNotEmpty } from '../empty.util';
@@ -52,7 +52,7 @@ function processProviderType(providers: Provider[], menuID: string, providerType
const childProviderTypes = (providerType as any).childProviderTypes;
childProviderTypes.forEach((childProviderType, childIndex: number) => {
processProviderType(providers, menuID, childProviderType, childIndex, `${providerPart.name}`, hasSubProviders);
processProviderType(providers, menuID, childProviderType, childIndex, `${menuID}_${index}`, hasSubProviders);
});
processProviderType(providers, menuID, providerPart, index, parentID, true);
@@ -83,10 +83,10 @@ function addProviderToList(providers: Provider[], providerType: Type<AbstractMen
provider.menuID = menuID as MenuID;
provider.index = provider.index ?? index;
if (hasValue(parentID)) {
provider.menuProviderId = `${parentID}_${provider.constructor.name}`;
provider.parentID = parentID;
provider.menuProviderId = provider.menuProviderId ?? `${parentID}_${index}`;
provider.parentID = provider.parentID ?? parentID;
} else {
provider.menuProviderId = `${provider.constructor.name}`;
provider.menuProviderId = provider.menuProviderId ?? `${menuID}_${index}`;
}
if (isNotEmpty(paths)) {
provider.activePaths = paths;

View File

@@ -14,8 +14,11 @@ 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 { AbstractExpandableMenuProvider, } from './helper-providers/expandable-menu-provider';
import { PartialMenuSection } from '../menu-provider';
import { PartialMenuSection } from '../menu-provider.model';
/**
* Menu provider to create Access Control related menu sections
*/
@Injectable()
export class AccessControlMenuProvider extends AbstractExpandableMenuProvider {

View File

@@ -11,8 +11,11 @@ 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';
import { AbstractMenuProvider, PartialMenuSection, } from '../menu-provider.model';
/**
* Menu provider to create the Admin search menu section
*/
@Injectable()
export class AdminSearchMenuProvider extends AbstractMenuProvider {
constructor(

View File

@@ -17,8 +17,11 @@ import { getFirstSucceededRemoteData } from '../../../core/shared/operators';
import { MenuItemType } from '../menu-item-type.model';
import { TextMenuItemModel } from '../menu-item/models/text.model';
import { AbstractExpandableMenuProvider, } from './helper-providers/expandable-menu-provider';
import { PartialMenuSection } from '../menu-provider';
import { PartialMenuSection } from '../menu-provider.model';
/**
* Menu provider to create the browse menu sections
*/
@Injectable()
export class BrowseMenuProvider extends AbstractExpandableMenuProvider {
constructor(

View File

@@ -14,10 +14,13 @@ import { FeatureID } from '../../../core/data/feature-authorization/feature-id';
import { SubscriptionModalComponent } from '../../subscriptions/subscription-modal/subscription-modal.component';
import { MenuItemType } from '../menu-item-type.model';
import { OnClickMenuItemModel } from '../menu-item/models/onclick.model';
import { PartialMenuSection } from '../menu-provider';
import { PartialMenuSection } from '../menu-provider.model';
import { DSpaceObjectPageMenuProvider } from './helper-providers/dso.menu';
import { DSpaceObject } from '../../../core/shared/dspace-object.model';
/**
* Menu provider to create the subscribe menu section
*/
@Injectable()
export class SubscribeMenuProvider extends DSpaceObjectPageMenuProvider {
constructor(

View File

@@ -9,8 +9,11 @@
import { Injectable } from '@angular/core';
import { Observable, of, } from 'rxjs';
import { MenuItemType } from '../menu-item-type.model';
import { AbstractMenuProvider, PartialMenuSection, } from '../menu-provider';
import { AbstractMenuProvider, PartialMenuSection, } from '../menu-provider.model';
/**
* Menu provider to create the community list menu section
*/
@Injectable()
export class CommunityListMenuProvider extends AbstractMenuProvider {
public getSections(): Observable<PartialMenuSection[]> {

View File

@@ -12,8 +12,11 @@ import { AuthorizationDataService } from '../../../core/data/feature-authorizati
import { FeatureID } from '../../../core/data/feature-authorization/feature-id';
import { MenuItemType } from '../menu-item-type.model';
import { LinkMenuItemModel } from '../menu-item/models/link.model';
import { AbstractMenuProvider, PartialMenuSection, } from '../menu-provider';
import { AbstractMenuProvider, PartialMenuSection, } from '../menu-provider.model';
/**
* Menu provider to create the curation menu section
*/
@Injectable()
export class CurationMenuProvider extends AbstractMenuProvider {
constructor(

View File

@@ -15,9 +15,12 @@ import { DSpaceObject } from '../../../core/shared/dspace-object.model';
import { URLCombiner } from '../../../core/url-combiner/url-combiner';
import { MenuItemType } from '../menu-item-type.model';
import { LinkMenuItemModel } from '../menu-item/models/link.model';
import { PartialMenuSection } from '../menu-provider';
import { PartialMenuSection } from '../menu-provider.model';
import { DSpaceObjectPageMenuProvider } from './helper-providers/dso.menu';
/**
* Menu provider to create the DSO edit menu section
*/
@Injectable()
export class DSpaceObjectEditMenuProvider extends DSpaceObjectPageMenuProvider {
constructor(

View File

@@ -9,11 +9,15 @@
import { Injectable } from '@angular/core';
import { Observable, of, } from 'rxjs';
import { MenuItemType } from '../menu-item-type.model';
import { PartialMenuSection } from '../menu-provider';
import { PartialMenuSection } from '../menu-provider.model';
import { DSpaceObjectPageMenuProvider } from './helper-providers/dso.menu';
import { DSpaceObject } from 'src/app/core/shared/dspace-object.model';
import { hasValue } from '../../empty.util';
/**
* Menu provider to create the parent wrapper menu of the various DSO page menu sections
* This section will be rendered as a button on the DSO pages if sub providers have been added
*/
@Injectable()
export class DsoOptionMenu extends DSpaceObjectPageMenuProvider {

View File

@@ -22,8 +22,11 @@ import {
} from '../../dso-selector/modal-wrappers/edit-item-selector/themed-edit-item-selector.component';
import { MenuItemType } from '../menu-item-type.model';
import { AbstractExpandableMenuProvider, } from './helper-providers/expandable-menu-provider';
import { PartialMenuSection } from '../menu-provider';
import { PartialMenuSection } from '../menu-provider.model';
/**
* Menu provider to create the admin sidebar edit menu sections
*/
@Injectable()
export class EditMenuProvider extends AbstractExpandableMenuProvider {
constructor(

View File

@@ -20,8 +20,11 @@ import {
} from '../../dso-selector/modal-wrappers/export-metadata-selector/export-metadata-selector.component';
import { MenuItemType } from '../menu-item-type.model';
import { AbstractExpandableMenuProvider, } from './helper-providers/expandable-menu-provider';
import { PartialMenuSection } from '../menu-provider';
import { PartialMenuSection } from '../menu-provider.model';
/**
* Menu provider to create the export menu sections
*/
@Injectable()
export class ExportMenuProvider extends AbstractExpandableMenuProvider {
constructor(

View File

@@ -11,8 +11,11 @@ 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';
import { AbstractMenuProvider, PartialMenuSection, } from '../menu-provider.model';
/**
* Menu provider to create the health menu section
*/
@Injectable()
export class HealthMenuProvider extends AbstractMenuProvider {
constructor(

View File

@@ -12,8 +12,14 @@ import { AbstractRouteContextMenuProvider } from './route-context.menu';
import { RemoteData } from '../../../../core/data/remote-data';
import { hasValue } from '../../../empty.util';
/**
* 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> {
const dsoRD: RemoteData<DSpaceObject> = route.data.dso;
if (hasValue(dsoRD) && dsoRD.hasSucceeded && hasValue(dsoRD.payload)) {
@@ -24,7 +30,7 @@ export abstract class DSpaceObjectPageMenuProvider extends AbstractRouteContextM
}
/**
* Retrieve the dso or entity type for an object to be used in generic messages
* Retrieve the dso or entity type for an object to be used in section messages
*/
protected getDsoType(dso: DSpaceObject) {
const renderType = dso.getRenderTypes()[0];

View File

@@ -5,29 +5,35 @@
*
* http://www.dspace.org/license/
*/
import { combineLatest, Observable, of as observableOf, } from 'rxjs';
import { combineLatest, Observable, } from 'rxjs';
import { map } from 'rxjs/operators';
import { AbstractMenuProvider, PartialMenuSection, } from '../../menu-provider';
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[]>;
protected includeSubSections(): boolean {
return true;
}
/**
* Retrieve all sections
* This method will combine both the top section and subsections
*/
getSections(): Observable<PartialMenuSection[]> {
const full = this.includeSubSections();
return combineLatest([
this.getTopSection(),
full ? this.getSubSections() : observableOf([]),
this.getSubSections(),
]).pipe(
map((
[partialTopSection, partialSubSections]: [PartialMenuSection, PartialMenuSection[]]
@@ -35,8 +41,9 @@ export abstract class AbstractExpandableMenuProvider extends AbstractMenuProvide
const subSections = partialSubSections.map((partialSub, index) => {
return {
...partialSub,
id: partialSub.id ?? `${this.menuProviderId}_Sub-${index}`,
id: partialSub.id ?? `${this.menuProviderId}_${index}`,
parentID: this.menuProviderId,
alwaysRenderExpandable: false,
};
});
@@ -45,6 +52,7 @@ export abstract class AbstractExpandableMenuProvider extends AbstractMenuProvide
{
...partialTopSection,
id: this.menuProviderId,
alwaysRenderExpandable: this.alwaysRenderExpandable,
},
];
})

View File

@@ -8,8 +8,11 @@
import { ActivatedRouteSnapshot, RouterStateSnapshot, } from '@angular/router';
import { Observable, of as observableOf, } from 'rxjs';
import { switchMap } from 'rxjs/operators';
import { AbstractMenuProvider, PartialMenuSection, } from '../../menu-provider';
import { AbstractMenuProvider, PartialMenuSection, } from '../../menu-provider.model';
/**
* Helper provider for route dependent menus
*/
export abstract class AbstractRouteContextMenuProvider<T> extends AbstractMenuProvider {
shouldPersistOnRouteChange = false;
@@ -21,7 +24,7 @@ export abstract class AbstractRouteContextMenuProvider<T> extends AbstractMenuPr
return this.getRouteContext(route, state).pipe(
switchMap((routeContext: T) => {
if (this.isApplicable(routeContext)) {
if (this.isApplicable(routeContext)) {
return this.getSectionsForContext(routeContext);
} else {
return observableOf([]);

View File

@@ -14,8 +14,11 @@ 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 { AbstractExpandableMenuProvider, } from './helper-providers/expandable-menu-provider';
import { PartialMenuSection } from '../menu-provider';
import { PartialMenuSection } from '../menu-provider.model';
/**
* Menu provider to create the import menu sections
*/
@Injectable()
export class ImportMenuProvider extends AbstractExpandableMenuProvider {
constructor(

View File

@@ -19,11 +19,14 @@ import { NotificationsService } from '../../notifications/notifications.service'
import { MenuID } from '../menu-id.model';
import { MenuItemType } from '../menu-item-type.model';
import { OnClickMenuItemModel } from '../menu-item/models/onclick.model';
import { PartialMenuSection } from '../menu-provider';
import { PartialMenuSection } from '../menu-provider.model';
import { MenuService } from '../menu.service';
import { DSpaceObjectPageMenuProvider } from './helper-providers/dso.menu';
import { DSpaceObject } from '../../../core/shared/dspace-object.model';
/**
* Menu provider to create the menu section on person entity pages to claim a researcher by creating a profile
*/
@Injectable()
export class ClaimMenuProvider extends DSpaceObjectPageMenuProvider {
constructor(

View File

@@ -15,9 +15,12 @@ import { Item } from '../../../core/shared/item.model';
import { URLCombiner } from '../../../core/url-combiner/url-combiner';
import { MenuItemType } from '../menu-item-type.model';
import { LinkMenuItemModel } from '../menu-item/models/link.model';
import { PartialMenuSection } from '../menu-provider';
import { PartialMenuSection } from '../menu-provider.model';
import { DSpaceObjectPageMenuProvider } from './helper-providers/dso.menu';
/**
* Menu provider to create the Orcid synchronisation menu section on person entity pages
*/
@Injectable()
export class OrcidMenuProvider extends DSpaceObjectPageMenuProvider {
constructor(

View File

@@ -14,9 +14,12 @@ import { Item } from '../../../core/shared/item.model';
import { DsoVersioningModalService } from '../../dso-page/dso-versioning-modal-service/dso-versioning-modal.service';
import { MenuItemType } from '../menu-item-type.model';
import { OnClickMenuItemModel } from '../menu-item/models/onclick.model';
import { PartialMenuSection } from '../menu-provider';
import { PartialMenuSection } from '../menu-provider.model';
import { DSpaceObjectPageMenuProvider } from './helper-providers/dso.menu';
/**
* Menu provider to create the versioning menu section on item pages
*/
@Injectable()
export class VersioningMenuProvider extends DSpaceObjectPageMenuProvider {
constructor(

View File

@@ -23,8 +23,11 @@ import {
import { MenuItemType } from '../menu-item-type.model';
import { TextMenuItemModel } from '../menu-item/models/text.model';
import { AbstractExpandableMenuProvider, } from './helper-providers/expandable-menu-provider';
import { PartialMenuSection } from '../menu-provider';
import { PartialMenuSection } from '../menu-provider.model';
/**
* Menu provider to create the admin sidebar new menu sections
*/
@Injectable()
export class NewMenuProvider extends AbstractExpandableMenuProvider {
constructor(

View File

@@ -11,8 +11,11 @@ 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';
import { AbstractMenuProvider, PartialMenuSection, } from '../menu-provider.model';
/**
* Menu provider to create the scripts and processes menu section
*/
@Injectable()
export class ProcessesMenuProvider extends AbstractMenuProvider {
constructor(

View File

@@ -14,8 +14,11 @@ 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 { AbstractExpandableMenuProvider, } from './helper-providers/expandable-menu-provider';
import { PartialMenuSection } from '../menu-provider';
import { PartialMenuSection } from '../menu-provider.model';
/**
* Menu provider to create the registries menu sections
*/
@Injectable()
export class RegistriesMenuProvider extends AbstractExpandableMenuProvider {
constructor(

View File

@@ -11,17 +11,18 @@ import { ActivatedRouteSnapshot, RouterStateSnapshot, } from '@angular/router';
import { Observable, of, } from 'rxjs';
import { hasNoValue, hasValue } from '../../empty.util';
import { MenuItemType } from '../menu-item-type.model';
import { PartialMenuSection } from '../menu-provider';
import { PartialMenuSection } from '../menu-provider.model';
import { AbstractRouteContextMenuProvider } from './helper-providers/route-context.menu';
import { DSpaceObject } from '../../../core/shared/dspace-object.model';
import { RemoteData } from '../../../core/data/remote-data';
import { getDSORoute } from '../../../app-routing-paths';
interface StatisticsLink {
id: string,
link: string,
}
/**
* Menu provider to create the statistics menu section depending on the page it is on
* When the user is on a DSO page or a derivative, this menu section will contain a link to the statistics of that DSO
* In all other cases the menu section will contain a link to the repository wide statistics
*/
@Injectable()
export class StatisticsMenuProvider extends AbstractRouteContextMenuProvider<DSpaceObject> {

View File

@@ -11,8 +11,11 @@ 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';
import { AbstractMenuProvider, PartialMenuSection, } from '../menu-provider.model';
/**
* Menu provider to create the system wide alert menu section
*/
@Injectable()
export class SystemWideAlertMenuProvider extends AbstractMenuProvider {
constructor(

View File

@@ -11,8 +11,11 @@ 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';
import { AbstractMenuProvider, PartialMenuSection, } from '../menu-provider.model';
/**
* Menu provider to create the workflow admin menu section
*/
@Injectable()
export class WorkflowMenuProvider extends AbstractMenuProvider {
constructor(

View File

@@ -273,9 +273,6 @@ import {
AdvancedClaimedTaskActionRatingComponent
} from './mydspace-actions/claimed-task/rating/advanced-claimed-task-action-rating.component';
import { ClaimedTaskActionsDeclineTaskComponent } from './mydspace-actions/claimed-task/decline-task/claimed-task-actions-decline-task.component';
import {
DsoPageSubscriptionButtonComponent
} from './dso-page/dso-page-subscription-button/dso-page-subscription-button.component';
import { EpersonGroupListComponent } from './eperson-group-list/eperson-group-list.component';
import { EpersonSearchBoxComponent } from './eperson-group-list/eperson-search-box/eperson-search-box.component';
import { GroupSearchBoxComponent } from './eperson-group-list/group-search-box/group-search-box.component';
@@ -395,7 +392,6 @@ const COMPONENTS = [
ItemPageTitleFieldComponent,
ThemedSearchNavbarComponent,
ListableNotificationObjectComponent,
DsoPageSubscriptionButtonComponent,
MetadataFieldWrapperComponent,
ContextHelpWrapperComponent,
EpersonGroupListComponent,

View File

@@ -70,7 +70,7 @@ export class ServerInitService extends InitService {
this.initAngulartics();
this.initRouteListeners();
this.themeService.listenForThemeChanges(false);
// this.initPersistentMenus();
this.menuProviderService.initPersistentMenus();
await this.authenticationReady$().toPromise();