From a105131b2bb507fe840053b95f4047735041e9a3 Mon Sep 17 00:00:00 2001 From: Yana De Pauw Date: Fri, 15 Nov 2024 18:25:37 +0100 Subject: [PATCH] Finalise menu refactor, add typedocs and tests --- ...le-admin-sidebar-section.component.spec.ts | 133 ++-- src/app/app-routing.module.ts | 3 - src/app/browse-by/browse-by-routing.module.ts | 2 - src/app/menu.resolver.spec.ts | 423 ----------- src/app/menu.resolver.ts | 703 ------------------ ...xpandable-navbar-section.component.spec.ts | 6 +- .../dso-page/dso-edit-menu.resolver.spec.ts | 259 ------- .../shared/dso-page/dso-edit-menu.resolver.ts | 229 ------ ...-menu-expandable-section.component.spec.ts | 102 ++- .../dso-edit-menu.component.spec.ts | 1 - ...so-page-subscription-button.component.html | 8 - ...so-page-subscription-button.component.scss | 0 ...page-subscription-button.component.spec.ts | 83 --- .../dso-page-subscription-button.component.ts | 57 -- ...enu-provider.ts => menu-provider.model.ts} | 69 +- .../shared/menu/menu-provider.service.spec.ts | 165 ++++ src/app/shared/menu/menu-provider.service.ts | 75 +- src/app/shared/menu/menu-section.model.ts | 25 +- src/app/shared/menu/menu.component.spec.ts | 2 +- src/app/shared/menu/menu.resolver.ts | 19 - src/app/shared/menu/menu.service.spec.ts | 87 --- src/app/shared/menu/menu.service.ts | 97 +-- src/app/shared/menu/menu.structure.spec.ts | 102 +++ src/app/shared/menu/menu.structure.ts | 10 +- .../menu/providers/access-control.menu.ts | 5 +- .../menu/providers/admin-search.menu.ts | 5 +- src/app/shared/menu/providers/browse.menu.ts | 5 +- .../menu/providers/comcol-subscribe.menu.ts | 5 +- .../menu/providers/community-list.menu.ts | 5 +- .../shared/menu/providers/curation.menu.ts | 5 +- .../shared/menu/providers/dso-edit.menu.ts | 5 +- .../shared/menu/providers/dso-option.menu.ts | 6 +- src/app/shared/menu/providers/edit.menu.ts | 5 +- src/app/shared/menu/providers/export.menu.ts | 5 +- src/app/shared/menu/providers/health.menu.ts | 5 +- .../providers/helper-providers/dso.menu.ts | 8 +- .../expandable-menu-provider.ts | 30 +- .../helper-providers/route-context.menu.ts | 7 +- src/app/shared/menu/providers/import.menu.ts | 5 +- .../shared/menu/providers/item-claim.menu.ts | 5 +- .../shared/menu/providers/item-orcid.menu.ts | 5 +- .../menu/providers/item-versioning.menu.ts | 5 +- src/app/shared/menu/providers/new.menu.ts | 5 +- .../shared/menu/providers/processes.menu.ts | 5 +- .../shared/menu/providers/registries.menu.ts | 5 +- .../shared/menu/providers/statistics.menu.ts | 11 +- .../menu/providers/system-wide-alert.menu.ts | 5 +- .../shared/menu/providers/workflow.menu.ts | 5 +- src/app/shared/shared.module.ts | 4 - src/modules/app/server-init.service.ts | 2 +- 50 files changed, 668 insertions(+), 2155 deletions(-) delete mode 100644 src/app/menu.resolver.spec.ts delete mode 100644 src/app/menu.resolver.ts delete mode 100644 src/app/shared/dso-page/dso-edit-menu.resolver.spec.ts delete mode 100644 src/app/shared/dso-page/dso-edit-menu.resolver.ts delete mode 100644 src/app/shared/dso-page/dso-page-subscription-button/dso-page-subscription-button.component.html delete mode 100644 src/app/shared/dso-page/dso-page-subscription-button/dso-page-subscription-button.component.scss delete mode 100644 src/app/shared/dso-page/dso-page-subscription-button/dso-page-subscription-button.component.spec.ts delete mode 100644 src/app/shared/dso-page/dso-page-subscription-button/dso-page-subscription-button.component.ts rename src/app/shared/menu/{menu-provider.ts => menu-provider.model.ts} (52%) create mode 100644 src/app/shared/menu/menu-provider.service.spec.ts delete mode 100644 src/app/shared/menu/menu.resolver.ts create mode 100644 src/app/shared/menu/menu.structure.spec.ts diff --git a/src/app/admin/admin-sidebar/expandable-admin-sidebar-section/expandable-admin-sidebar-section.component.spec.ts b/src/app/admin/admin-sidebar/expandable-admin-sidebar-section/expandable-admin-sidebar-section.component.spec.ts index dd31f757c2..18d042e995 100644 --- a/src/app/admin/admin-sidebar/expandable-admin-sidebar-section/expandable-admin-sidebar-section.component.spec.ts +++ b/src/app/admin/admin-sidebar/expandable-admin-sidebar-section/expandable-admin-sidebar-section.component.spec.ts @@ -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; 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(); }); }); }); diff --git a/src/app/app-routing.module.ts b/src/app/app-routing.module.ts index 4af778433d..614e04556a 100644 --- a/src/app/app-routing.module.ts +++ b/src/app/app-routing.module.ts @@ -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' }, diff --git a/src/app/browse-by/browse-by-routing.module.ts b/src/app/browse-by/browse-by-routing.module.ts index bb67dc65ae..5f39a4b24e 100644 --- a/src/app/browse-by/browse-by-routing.module.ts +++ b/src/app/browse-by/browse-by-routing.module.ts @@ -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: [ { diff --git a/src/app/menu.resolver.spec.ts b/src/app/menu.resolver.spec.ts deleted file mode 100644 index 838d5a53c5..0000000000 --- a/src/app/menu.resolver.spec.ts +++ /dev/null @@ -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, - })); - }); - }); - }); -}); diff --git a/src/app/menu.resolver.ts b/src/app/menu.resolver.ts deleted file mode 100644 index cad6a6ec57..0000000000 --- a/src/app/menu.resolver.ts +++ /dev/null @@ -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 { - 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 { - 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 { - 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 { - 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>()) - .subscribe((browseDefListRD: RemoteData>) => { - 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, - }))); - }); - } -} diff --git a/src/app/navbar/expandable-navbar-section/expandable-navbar-section.component.spec.ts b/src/app/navbar/expandable-navbar-section/expandable-navbar-section.component.spec.ts index 488c9ab251..ef7eee9868 100644 --- a/src/app/navbar/expandable-navbar-section/expandable-navbar-section.component.spec.ts +++ b/src/app/navbar/expandable-navbar-section/expandable-navbar-section.component.spec.ts @@ -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: () => {/**/ diff --git a/src/app/shared/dso-page/dso-edit-menu.resolver.spec.ts b/src/app/shared/dso-page/dso-edit-menu.resolver.spec.ts deleted file mode 100644 index abfe618174..0000000000 --- a/src/app/shared/dso-page/dso-edit-menu.resolver.spec.ts +++ /dev/null @@ -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(); - }); - - }); - }); -}); diff --git a/src/app/shared/dso-page/dso-edit-menu.resolver.ts b/src/app/shared/dso-page/dso-edit-menu.resolver.ts deleted file mode 100644 index 80a69c2830..0000000000 --- a/src/app/shared/dso-page/dso-edit-menu.resolver.ts +++ /dev/null @@ -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[] { - return [ - this.getItemMenu(dso), - this.getCommonMenu(dso, state) - ]; - } - - /** - * Get the common menus between all dspace objects - */ - protected getCommonMenu(dso, state): Observable { - 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 { - 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; - }); - } -} diff --git a/src/app/shared/dso-page/dso-edit-menu/dso-edit-expandable-menu-section/dso-edit-menu-expandable-section.component.spec.ts b/src/app/shared/dso-page/dso-edit-menu/dso-edit-expandable-menu-section/dso-edit-menu-expandable-section.component.spec.ts index 79ab35bd28..613f06f6ad 100644 --- a/src/app/shared/dso-page/dso-edit-menu/dso-edit-expandable-menu-section/dso-edit-menu-expandable-section.component.spec.ts +++ b/src/app/shared/dso-page/dso-edit-menu/dso-edit-expandable-menu-section/dso-edit-menu-expandable-section.component.spec.ts @@ -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(); + }); }); }); diff --git a/src/app/shared/dso-page/dso-edit-menu/dso-edit-menu.component.spec.ts b/src/app/shared/dso-page/dso-edit-menu/dso-edit-menu.component.spec.ts index d7f0ead878..71f8355eb7 100644 --- a/src/app/shared/dso-page/dso-edit-menu/dso-edit-menu.component.spec.ts +++ b/src/app/shared/dso-page/dso-edit-menu/dso-edit-menu.component.spec.ts @@ -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'; diff --git a/src/app/shared/dso-page/dso-page-subscription-button/dso-page-subscription-button.component.html b/src/app/shared/dso-page/dso-page-subscription-button/dso-page-subscription-button.component.html deleted file mode 100644 index 15135009fc..0000000000 --- a/src/app/shared/dso-page/dso-page-subscription-button/dso-page-subscription-button.component.html +++ /dev/null @@ -1,8 +0,0 @@ - diff --git a/src/app/shared/dso-page/dso-page-subscription-button/dso-page-subscription-button.component.scss b/src/app/shared/dso-page/dso-page-subscription-button/dso-page-subscription-button.component.scss deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/src/app/shared/dso-page/dso-page-subscription-button/dso-page-subscription-button.component.spec.ts b/src/app/shared/dso-page/dso-page-subscription-button/dso-page-subscription-button.component.spec.ts deleted file mode 100644 index 726854778d..0000000000 --- a/src/app/shared/dso-page/dso-page-subscription-button/dso-page-subscription-button.component.spec.ts +++ /dev/null @@ -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; - 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(); - }); - }); -}); diff --git a/src/app/shared/dso-page/dso-page-subscription-button/dso-page-subscription-button.component.ts b/src/app/shared/dso-page/dso-page-subscription-button/dso-page-subscription-button.component.ts deleted file mode 100644 index 54cd9e6bb0..0000000000 --- a/src/app/shared/dso-page/dso-page-subscription-button/dso-page-subscription-button.component.ts +++ /dev/null @@ -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 = 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; - } - -} diff --git a/src/app/shared/menu/menu-provider.ts b/src/app/shared/menu/menu-provider.model.ts similarity index 52% rename from src/app/shared/menu/menu-provider.ts rename to src/app/shared/menu/menu-provider.model.ts index 7044870f17..247963f172 100644 --- a/src/app/shared/menu/menu-provider.ts +++ b/src/app/shared/menu/menu-provider.model.ts @@ -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; } +/** + * 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; 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 | 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; - protected concat(...sections$: Observable[]): Observable { - return combineLatest(sections$).pipe( - map(sections => flatten(sections)), - ); - } } diff --git a/src/app/shared/menu/menu-provider.service.spec.ts b/src/app/shared/menu/menu-provider.service.spec.ts new file mode 100644 index 0000000000..44e839bd17 --- /dev/null +++ b/src/app/shared/menu/menu-provider.service.spec.ts @@ -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); + }); + }); + + +}); + diff --git a/src/app/shared/menu/menu-provider.service.ts b/src/app/shared/menu/menu-provider.service.ts index a984e594f8..9b5d5e73a5 100644 --- a/src/app/shared/menu/menu-provider.service.ts +++ b/src/app/shared/menu/menu-provider.service.ts @@ -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, 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); }); diff --git a/src/app/shared/menu/menu-section.model.ts b/src/app/shared/menu/menu-section.model.ts index 700f179a3f..a91d75302d 100644 --- a/src/app/shared/menu/menu-section.model.ts +++ b/src/app/shared/menu/menu-section.model.ts @@ -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; } diff --git a/src/app/shared/menu/menu.component.spec.ts b/src/app/shared/menu/menu.component.spec.ts index 707b1c0746..19035b6028 100644 --- a/src/app/shared/menu/menu.component.spec.ts +++ b/src/app/shared/menu/menu.component.spec.ts @@ -32,7 +32,7 @@ describe('MenuComponent', () => { } as TextMenuItemModel, icon: 'globe', visible: true, - } + }; const mockMenuID = 'mock-menuID' as MenuID; diff --git a/src/app/shared/menu/menu.resolver.ts b/src/app/shared/menu/menu.resolver.ts deleted file mode 100644 index 9f01934670..0000000000 --- a/src/app/shared/menu/menu.resolver.ts +++ /dev/null @@ -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 { -// return ( -// route: ActivatedRouteSnapshot, -// state: RouterStateSnapshot, -// menuProviderService: MenuProviderService = inject(MenuProviderService), -// ) => menuProviderService.resolveStaticMenu(); -// } diff --git a/src/app/shared/menu/menu.service.spec.ts b/src/app/shared/menu/menu.service.spec.ts index 0d8d669a0a..d4cdb7035d 100644 --- a/src/app/shared/menu/menu.service.spec.ts +++ b/src/app/shared/menu/menu.service.spec.ts @@ -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); - }); - }); - }); diff --git a/src/app/shared/menu/menu.service.ts b/src/app/shared/menu/menu.service.ts index ed367a1fc3..6a1c4a5752 100644 --- a/src/app/shared/menu/menu.service.ts +++ b/src/app/shared/menu/menu.service.ts @@ -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(key: string, selector): MemoizedSelector { 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; - } - - } diff --git a/src/app/shared/menu/menu.structure.spec.ts b/src/app/shared/menu/menu.structure.spec.ts new file mode 100644 index 0000000000..26328510f6 --- /dev/null +++ b/src/app/shared/menu/menu.structure.spec.ts @@ -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); + }); + }); +}); diff --git a/src/app/shared/menu/menu.structure.ts b/src/app/shared/menu/menu.structure.ts index bc58f70391..581fff6c9a 100644 --- a/src/app/shared/menu/menu.structure.ts +++ b/src/app/shared/menu/menu.structure.ts @@ -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 { diff --git a/src/app/shared/menu/providers/curation.menu.ts b/src/app/shared/menu/providers/curation.menu.ts index 1c35e3fcd8..c6ecf38494 100644 --- a/src/app/shared/menu/providers/curation.menu.ts +++ b/src/app/shared/menu/providers/curation.menu.ts @@ -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( diff --git a/src/app/shared/menu/providers/dso-edit.menu.ts b/src/app/shared/menu/providers/dso-edit.menu.ts index 5d5a480a92..9b341904a3 100644 --- a/src/app/shared/menu/providers/dso-edit.menu.ts +++ b/src/app/shared/menu/providers/dso-edit.menu.ts @@ -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( diff --git a/src/app/shared/menu/providers/dso-option.menu.ts b/src/app/shared/menu/providers/dso-option.menu.ts index 4c5ab9c625..3313f7ebca 100644 --- a/src/app/shared/menu/providers/dso-option.menu.ts +++ b/src/app/shared/menu/providers/dso-option.menu.ts @@ -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 { diff --git a/src/app/shared/menu/providers/edit.menu.ts b/src/app/shared/menu/providers/edit.menu.ts index d72b00ac15..000959edb1 100644 --- a/src/app/shared/menu/providers/edit.menu.ts +++ b/src/app/shared/menu/providers/edit.menu.ts @@ -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( diff --git a/src/app/shared/menu/providers/export.menu.ts b/src/app/shared/menu/providers/export.menu.ts index 77f7504a8e..6a19929fcb 100644 --- a/src/app/shared/menu/providers/export.menu.ts +++ b/src/app/shared/menu/providers/export.menu.ts @@ -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( diff --git a/src/app/shared/menu/providers/health.menu.ts b/src/app/shared/menu/providers/health.menu.ts index bc7eef597e..d7ff771f57 100644 --- a/src/app/shared/menu/providers/health.menu.ts +++ b/src/app/shared/menu/providers/health.menu.ts @@ -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( diff --git a/src/app/shared/menu/providers/helper-providers/dso.menu.ts b/src/app/shared/menu/providers/helper-providers/dso.menu.ts index f88e452aff..9b2d8642fa 100644 --- a/src/app/shared/menu/providers/helper-providers/dso.menu.ts +++ b/src/app/shared/menu/providers/helper-providers/dso.menu.ts @@ -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 { + /** + * Retrieve the dso from the current route data + */ public getRouteContext(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable { const dsoRD: RemoteData = 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]; diff --git a/src/app/shared/menu/providers/helper-providers/expandable-menu-provider.ts b/src/app/shared/menu/providers/helper-providers/expandable-menu-provider.ts index 276b892e6e..e95aa80103 100644 --- a/src/app/shared/menu/providers/helper-providers/expandable-menu-provider.ts +++ b/src/app/shared/menu/providers/helper-providers/expandable-menu-provider.ts @@ -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; + /** + * Get the subsections for this expandable menu + */ abstract getSubSections(): Observable; - protected includeSubSections(): boolean { - return true; - } - + /** + * Retrieve all sections + * This method will combine both the top section and subsections + */ getSections(): Observable { - 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, }, ]; }) diff --git a/src/app/shared/menu/providers/helper-providers/route-context.menu.ts b/src/app/shared/menu/providers/helper-providers/route-context.menu.ts index 2ffc97ac5e..bd03bcc0f3 100644 --- a/src/app/shared/menu/providers/helper-providers/route-context.menu.ts +++ b/src/app/shared/menu/providers/helper-providers/route-context.menu.ts @@ -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 extends AbstractMenuProvider { shouldPersistOnRouteChange = false; @@ -21,7 +24,7 @@ export abstract class AbstractRouteContextMenuProvider 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([]); diff --git a/src/app/shared/menu/providers/import.menu.ts b/src/app/shared/menu/providers/import.menu.ts index 3e0cf3228e..8a758ae2c1 100644 --- a/src/app/shared/menu/providers/import.menu.ts +++ b/src/app/shared/menu/providers/import.menu.ts @@ -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( diff --git a/src/app/shared/menu/providers/item-claim.menu.ts b/src/app/shared/menu/providers/item-claim.menu.ts index f2afb70ea7..92b27fc02e 100644 --- a/src/app/shared/menu/providers/item-claim.menu.ts +++ b/src/app/shared/menu/providers/item-claim.menu.ts @@ -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( diff --git a/src/app/shared/menu/providers/item-orcid.menu.ts b/src/app/shared/menu/providers/item-orcid.menu.ts index 1416b54a34..208a3b26c6 100644 --- a/src/app/shared/menu/providers/item-orcid.menu.ts +++ b/src/app/shared/menu/providers/item-orcid.menu.ts @@ -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( diff --git a/src/app/shared/menu/providers/item-versioning.menu.ts b/src/app/shared/menu/providers/item-versioning.menu.ts index 15349a7016..9e30a16998 100644 --- a/src/app/shared/menu/providers/item-versioning.menu.ts +++ b/src/app/shared/menu/providers/item-versioning.menu.ts @@ -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( diff --git a/src/app/shared/menu/providers/new.menu.ts b/src/app/shared/menu/providers/new.menu.ts index 07c54fa9cb..6962970457 100644 --- a/src/app/shared/menu/providers/new.menu.ts +++ b/src/app/shared/menu/providers/new.menu.ts @@ -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( diff --git a/src/app/shared/menu/providers/processes.menu.ts b/src/app/shared/menu/providers/processes.menu.ts index 6943572beb..0aa3c03071 100644 --- a/src/app/shared/menu/providers/processes.menu.ts +++ b/src/app/shared/menu/providers/processes.menu.ts @@ -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( diff --git a/src/app/shared/menu/providers/registries.menu.ts b/src/app/shared/menu/providers/registries.menu.ts index d69aa79d0e..6464d1d48b 100644 --- a/src/app/shared/menu/providers/registries.menu.ts +++ b/src/app/shared/menu/providers/registries.menu.ts @@ -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( diff --git a/src/app/shared/menu/providers/statistics.menu.ts b/src/app/shared/menu/providers/statistics.menu.ts index 2d29cc575c..4690cdf1aa 100644 --- a/src/app/shared/menu/providers/statistics.menu.ts +++ b/src/app/shared/menu/providers/statistics.menu.ts @@ -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 { diff --git a/src/app/shared/menu/providers/system-wide-alert.menu.ts b/src/app/shared/menu/providers/system-wide-alert.menu.ts index e038a9c757..99d3a14a6c 100644 --- a/src/app/shared/menu/providers/system-wide-alert.menu.ts +++ b/src/app/shared/menu/providers/system-wide-alert.menu.ts @@ -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( diff --git a/src/app/shared/menu/providers/workflow.menu.ts b/src/app/shared/menu/providers/workflow.menu.ts index bcdeb424a8..ec075bc52b 100644 --- a/src/app/shared/menu/providers/workflow.menu.ts +++ b/src/app/shared/menu/providers/workflow.menu.ts @@ -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( diff --git a/src/app/shared/shared.module.ts b/src/app/shared/shared.module.ts index 0f7871f7f9..c6e2ddc3f3 100644 --- a/src/app/shared/shared.module.ts +++ b/src/app/shared/shared.module.ts @@ -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, diff --git a/src/modules/app/server-init.service.ts b/src/modules/app/server-init.service.ts index c7abf6d085..20b294cc27 100644 --- a/src/modules/app/server-init.service.ts +++ b/src/modules/app/server-init.service.ts @@ -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();