From 0291942613e7627a03d882b1bba6a88a30e1164b Mon Sep 17 00:00:00 2001 From: Yury Bondarenko Date: Thu, 28 Dec 2023 13:46:39 +0100 Subject: [PATCH 01/32] Proof-of-concept: independent menu section providers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Replace god-class resolvers with a service that populates the menus from lists of injectable providers - Static menu sections are resolved at the root route ~ `resolveStatic` - Route-dependent menu sections can be declared in the same structure, but are resolved on-demand ~ `resolveRoute` - More and more easily customizable - Parts can be moved between menus, removed, replaced or extended individually - The dependencies of each provider are independent of each other - Order of providers determines the order of each menu → single source of truth for the order --- .../admin-sidebar-section.component.ts | 11 +- ...dable-admin-sidebar-section.component.html | 2 +- ...andable-admin-sidebar-section.component.ts | 5 +- src/app/app-routing.module.ts | 40 +++--- src/app/app.menus.ts | 58 ++++++++ src/app/app.module.ts | 60 ++++++--- .../collection-page-routing.module.ts | 52 +++----- .../community-page-routing.module.ts | 47 +++---- src/app/home-page/home-page-routing.module.ts | 19 +-- src/app/item-page/item-page-routing.module.ts | 62 +++++---- src/app/item-page/item-page.resolver.ts | 4 +- .../expandable-navbar-section.component.ts | 12 +- .../navbar-section.component.ts | 14 +- ...dit-menu-expandable-section.component.scss | 14 +- ...-edit-menu-expandable-section.component.ts | 10 +- .../dso-edit-menu-section.component.ts | 10 +- .../menu/menu-item/models/altmetric.model.ts | 2 +- .../menu-item/models/external-link.model.ts | 2 +- .../menu/menu-item/models/link.model.ts | 2 +- .../menu/menu-item/models/menu-item.model.ts | 2 +- .../menu/menu-item/models/onclick.model.ts | 4 +- .../menu/menu-item/models/search.model.ts | 4 +- .../menu/menu-item/models/text.model.ts | 2 +- src/app/shared/menu/menu-provider.service.ts | 126 ++++++++++++++++++ src/app/shared/menu/menu-provider.ts | 47 +++++++ src/app/shared/menu/menu-section.model.ts | 93 +++++++++++-- ....ts => abstract-menu-section.component.ts} | 41 ++++-- .../menu-section.component.spec.ts | 37 +++-- src/app/shared/menu/menu-state.model.ts | 2 +- src/app/shared/menu/menu.component.ts | 10 +- src/app/shared/menu/menu.module.ts | 16 +-- src/app/shared/menu/menu.reducer.spec.ts | 2 +- src/app/shared/menu/menu.reducer.ts | 2 +- src/app/shared/menu/menu.resolver.ts | 35 +++++ src/app/shared/menu/menu.structure.ts | 46 +++++++ .../menu/providers/access-control.menu.ts | 94 +++++++++++++ .../menu/providers/admin-search.menu.ts | 50 +++++++ src/app/shared/menu/providers/browse.menu.ts | 67 ++++++++++ .../menu/providers/comcol-subscribe.menu.ts | 65 +++++++++ .../menu/providers/community-list.menu.ts | 35 +++++ .../shared/menu/providers/curation.menu.ts | 51 +++++++ .../shared/menu/providers/dso-edit.menu.ts | 53 ++++++++ src/app/shared/menu/providers/dso.menu.ts | 56 ++++++++ src/app/shared/menu/providers/edit.menu.ts | 92 +++++++++++++ .../providers/expandable-menu-provider.ts | 64 +++++++++ src/app/shared/menu/providers/export.menu.ts | 88 ++++++++++++ src/app/shared/menu/providers/health.menu.ts | 50 +++++++ src/app/shared/menu/providers/import.menu.ts | 80 +++++++++++ .../shared/menu/providers/item-claim.menu.ts | 93 +++++++++++++ .../shared/menu/providers/item-orcid.menu.ts | 57 ++++++++ .../menu/providers/item-versioning.menu.ts | 60 +++++++++ src/app/shared/menu/providers/item.menu.ts | 52 ++++++++ src/app/shared/menu/providers/new.menu.ts | 100 ++++++++++++++ .../menu/providers/notifications.menu.ts | 68 ++++++++++ .../shared/menu/providers/processes.menu.ts | 50 +++++++ .../shared/menu/providers/registries.menu.ts | 75 +++++++++++ .../menu/providers/route-context.menu.ts | 43 ++++++ .../shared/menu/providers/statistics.menu.ts | 70 ++++++++++ .../menu/providers/system-wide-alert.menu.ts | 50 +++++++ .../shared/menu/providers/workflow.menu.ts | 50 +++++++ 60 files changed, 2281 insertions(+), 227 deletions(-) create mode 100644 src/app/app.menus.ts create mode 100644 src/app/shared/menu/menu-provider.service.ts create mode 100644 src/app/shared/menu/menu-provider.ts rename src/app/shared/menu/menu-section/{menu-section.component.ts => abstract-menu-section.component.ts} (89%) create mode 100644 src/app/shared/menu/menu.resolver.ts create mode 100644 src/app/shared/menu/menu.structure.ts create mode 100644 src/app/shared/menu/providers/access-control.menu.ts create mode 100644 src/app/shared/menu/providers/admin-search.menu.ts create mode 100644 src/app/shared/menu/providers/browse.menu.ts create mode 100644 src/app/shared/menu/providers/comcol-subscribe.menu.ts create mode 100644 src/app/shared/menu/providers/community-list.menu.ts create mode 100644 src/app/shared/menu/providers/curation.menu.ts create mode 100644 src/app/shared/menu/providers/dso-edit.menu.ts create mode 100644 src/app/shared/menu/providers/dso.menu.ts create mode 100644 src/app/shared/menu/providers/edit.menu.ts create mode 100644 src/app/shared/menu/providers/expandable-menu-provider.ts create mode 100644 src/app/shared/menu/providers/export.menu.ts create mode 100644 src/app/shared/menu/providers/health.menu.ts create mode 100644 src/app/shared/menu/providers/import.menu.ts create mode 100644 src/app/shared/menu/providers/item-claim.menu.ts create mode 100644 src/app/shared/menu/providers/item-orcid.menu.ts create mode 100644 src/app/shared/menu/providers/item-versioning.menu.ts create mode 100644 src/app/shared/menu/providers/item.menu.ts create mode 100644 src/app/shared/menu/providers/new.menu.ts create mode 100644 src/app/shared/menu/providers/notifications.menu.ts create mode 100644 src/app/shared/menu/providers/processes.menu.ts create mode 100644 src/app/shared/menu/providers/registries.menu.ts create mode 100644 src/app/shared/menu/providers/route-context.menu.ts create mode 100644 src/app/shared/menu/providers/statistics.menu.ts create mode 100644 src/app/shared/menu/providers/system-wide-alert.menu.ts create mode 100644 src/app/shared/menu/providers/workflow.menu.ts diff --git a/src/app/admin/admin-sidebar/admin-sidebar-section/admin-sidebar-section.component.ts b/src/app/admin/admin-sidebar/admin-sidebar-section/admin-sidebar-section.component.ts index d6cd803622..3606cb299f 100644 --- a/src/app/admin/admin-sidebar/admin-sidebar-section/admin-sidebar-section.component.ts +++ b/src/app/admin/admin-sidebar/admin-sidebar-section/admin-sidebar-section.component.ts @@ -1,5 +1,5 @@ import { Component, Inject, Injector, OnInit } from '@angular/core'; -import { MenuSectionComponent } from '../../../shared/menu/menu-section/menu-section.component'; +import { AbstractMenuSectionComponent } from '../../../shared/menu/menu-section/abstract-menu-section.component'; import { MenuService } from '../../../shared/menu/menu.service'; import { rendersSectionForMenu } from '../../../shared/menu/menu-section.decorator'; import { LinkMenuItemModel } from '../../../shared/menu/menu-item/models/link.model'; @@ -19,7 +19,7 @@ import { Router } from '@angular/router'; }) @rendersSectionForMenu(MenuID.ADMIN, false) -export class AdminSidebarSectionComponent extends MenuSectionComponent implements OnInit { +export class AdminSidebarSectionComponent extends AbstractMenuSectionComponent implements OnInit { /** * This section resides in the Admin Sidebar @@ -33,16 +33,17 @@ export class AdminSidebarSectionComponent extends MenuSectionComponent implement isDisabled: boolean; constructor( - @Inject('sectionDataProvider') menuSection: MenuSection, + @Inject('sectionDataProvider') protected section: MenuSection, protected menuService: MenuService, protected injector: Injector, protected router: Router, ) { - super(menuSection, menuService, injector); - this.itemModel = menuSection.model as LinkMenuItemModel; + super(menuService, injector); + this.itemModel = section.model as LinkMenuItemModel; } ngOnInit(): void { + // todo: should support all menu entries? this.isDisabled = this.itemModel?.disabled || isEmpty(this.itemModel?.link); super.ngOnInit(); } diff --git a/src/app/admin/admin-sidebar/expandable-admin-sidebar-section/expandable-admin-sidebar-section.component.html b/src/app/admin/admin-sidebar/expandable-admin-sidebar-section/expandable-admin-sidebar-section.component.html index 1f4666bbd0..23f5a958d2 100644 --- a/src/app/admin/admin-sidebar/expandable-admin-sidebar-section/expandable-admin-sidebar-section.component.html +++ b/src/app/admin/admin-sidebar/expandable-admin-sidebar-section/expandable-admin-sidebar-section.component.html @@ -13,7 +13,7 @@ (keyup.enter)="toggleSection($event)" >
- +
diff --git a/src/app/navbar/expandable-navbar-section/expandable-navbar-section.component.ts b/src/app/navbar/expandable-navbar-section/expandable-navbar-section.component.ts index 6f05eb0e54..b56a29f115 100644 --- a/src/app/navbar/expandable-navbar-section/expandable-navbar-section.component.ts +++ b/src/app/navbar/expandable-navbar-section/expandable-navbar-section.component.ts @@ -3,10 +3,12 @@ import { MenuSection } from '../../shared/menu/menu-section.model'; import { NavbarSectionComponent } from '../navbar-section/navbar-section.component'; import { MenuService } from '../../shared/menu/menu.service'; import { slide } from '../../shared/animations/slide'; -import { first } from 'rxjs/operators'; +import { first, map } from 'rxjs/operators'; import { HostWindowService } from '../../shared/host-window.service'; import { rendersSectionForMenu } from '../../shared/menu/menu-section.decorator'; import { MenuID } from '../../shared/menu/menu-id.model'; +import { Observable } from 'rxjs'; +import { isNotEmpty } from '../../shared/empty.util'; /** * Represents an expandable section in the navbar @@ -24,6 +26,11 @@ export class ExpandableNavbarSectionComponent extends NavbarSectionComponent imp */ menuID = MenuID.PUBLIC; + /** + * Emits true when the top section has subsections, else emits false + */ + hasSubSections$: Observable; + constructor( @Inject('sectionDataProvider') protected section: MenuSection, protected menuService: MenuService, @@ -35,6 +42,9 @@ export class ExpandableNavbarSectionComponent extends NavbarSectionComponent imp ngOnInit() { super.ngOnInit(); + this.hasSubSections$ = this.subSections$.pipe( + map((subSections) => isNotEmpty(subSections)) + ); } /** diff --git a/src/app/shared/dso-page/dso-edit-menu/dso-edit-expandable-menu-section/dso-edit-menu-expandable-section.component.html b/src/app/shared/dso-page/dso-edit-menu/dso-edit-expandable-menu-section/dso-edit-menu-expandable-section.component.html index cb725e7d70..9c8c97ca6b 100644 --- a/src/app/shared/dso-page/dso-edit-menu/dso-edit-expandable-menu-section/dso-edit-menu-expandable-section.component.html +++ b/src/app/shared/dso-page/dso-edit-menu/dso-edit-expandable-menu-section/dso-edit-menu-expandable-section.component.html @@ -1,4 +1,4 @@ -
+
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(); From b2715501b04931925632478390203ce9e46bbdc5 Mon Sep 17 00:00:00 2001 From: Yury Bondarenko Date: Mon, 25 Nov 2024 16:49:12 +0100 Subject: [PATCH 08/32] Update tests to match --- .../shared/menu/providers/access-control.menu.spec.ts | 7 ++++--- .../shared/menu/providers/admin-search.menu.spec.ts | 2 +- src/app/shared/menu/providers/browse.menu.spec.ts | 7 ++++--- .../shared/menu/providers/community-list.menu.spec.ts | 2 +- src/app/shared/menu/providers/curation.menu.spec.ts | 2 +- src/app/shared/menu/providers/edit.menu.spec.ts | 7 ++++--- src/app/shared/menu/providers/export.menu.spec.ts | 10 ++++------ src/app/shared/menu/providers/health.menu.spec.ts | 2 +- src/app/shared/menu/providers/import.menu.spec.ts | 8 ++++---- src/app/shared/menu/providers/new.menu.spec.ts | 7 ++++--- src/app/shared/menu/providers/processes.menu.spec.ts | 2 +- src/app/shared/menu/providers/registries.menu.spec.ts | 7 ++++--- .../menu/providers/system-wide-alert.menu.spec.ts | 2 +- src/app/shared/menu/providers/workflow.menu.spec.ts | 2 +- 14 files changed, 35 insertions(+), 32 deletions(-) diff --git a/src/app/shared/menu/providers/access-control.menu.spec.ts b/src/app/shared/menu/providers/access-control.menu.spec.ts index d9284a75c4..878025065b 100644 --- a/src/app/shared/menu/providers/access-control.menu.spec.ts +++ b/src/app/shared/menu/providers/access-control.menu.spec.ts @@ -8,7 +8,7 @@ import { TestBed } from '@angular/core/testing'; import { MenuItemType } from '../menu-item-type.model'; -import { MenuSubSection, MenuTopSection } from './expandable-menu-provider'; +import { PartialMenuSection } from '../menu-provider.model'; import { AuthorizationDataServiceStub } from '../../testing/authorization-service.stub'; import { of as observableOf } from 'rxjs'; import { AuthorizationDataService } from '../../../core/data/feature-authorization/authorization-data.service'; @@ -17,7 +17,8 @@ import { AccessControlMenuProvider } from './access-control.menu'; import { ScriptDataService } from '../../../core/data/processes/script-data.service'; import { ScriptServiceStub } from '../../testing/script-service.stub'; -const expectedTopSection: MenuTopSection = { +const expectedTopSection: PartialMenuSection = { + visible: true, model: { type: MenuItemType.TEXT, text: 'menu.section.access_control', @@ -25,7 +26,7 @@ const expectedTopSection: MenuTopSection = { icon: 'key' }; -const expectedSubSections: MenuSubSection[] = [ +const expectedSubSections: PartialMenuSection[] = [ { visible: true, model: { diff --git a/src/app/shared/menu/providers/admin-search.menu.spec.ts b/src/app/shared/menu/providers/admin-search.menu.spec.ts index 0471215578..6862419408 100644 --- a/src/app/shared/menu/providers/admin-search.menu.spec.ts +++ b/src/app/shared/menu/providers/admin-search.menu.spec.ts @@ -7,7 +7,7 @@ */ import { TestBed } from '@angular/core/testing'; -import { PartialMenuSection } from '../menu-provider'; +import { PartialMenuSection } from '../menu-provider.model'; import { MenuItemType } from '../menu-item-type.model'; import { AdminSearchMenuProvider } from './admin-search.menu'; import { AuthorizationDataServiceStub } from '../../testing/authorization-service.stub'; diff --git a/src/app/shared/menu/providers/browse.menu.spec.ts b/src/app/shared/menu/providers/browse.menu.spec.ts index 20ac0f92be..1715645b4d 100644 --- a/src/app/shared/menu/providers/browse.menu.spec.ts +++ b/src/app/shared/menu/providers/browse.menu.spec.ts @@ -9,7 +9,7 @@ import { TestBed } from '@angular/core/testing'; import { BrowseMenuProvider } from './browse.menu'; import { MenuItemType } from '../menu-item-type.model'; -import { MenuSubSection, MenuTopSection } from './expandable-menu-provider'; +import { PartialMenuSection } from '../menu-provider.model'; import { BrowseService } from '../../../core/browse/browse.service'; import { BrowseServiceStub } from '../../testing/browse-service.stub'; import { ObjectCacheService } from '../../../core/cache/object-cache.service'; @@ -18,7 +18,8 @@ import { BrowseDefinition } from '../../../core/shared/browse-definition.model'; import { createSuccessfulRemoteDataObject$ } from '../../remote-data.utils'; import { createPaginatedList } from '../../testing/utils.test'; -const expectedTopSection: MenuTopSection = { +const expectedTopSection: PartialMenuSection = { + visible: true, model: { type: MenuItemType.TEXT, text: 'menu.section.browse_global', @@ -26,7 +27,7 @@ const expectedTopSection: MenuTopSection = { icon: 'globe', }; -const expectedSubSections: MenuSubSection[] = [ +const expectedSubSections: PartialMenuSection[] = [ { visible: true, model: { diff --git a/src/app/shared/menu/providers/community-list.menu.spec.ts b/src/app/shared/menu/providers/community-list.menu.spec.ts index b0e037d228..16d6af4e45 100644 --- a/src/app/shared/menu/providers/community-list.menu.spec.ts +++ b/src/app/shared/menu/providers/community-list.menu.spec.ts @@ -8,7 +8,7 @@ import { TestBed } from '@angular/core/testing'; import { CommunityListMenuProvider } from './community-list.menu'; -import { PartialMenuSection } from '../menu-provider'; +import { PartialMenuSection } from '../menu-provider.model'; import { MenuItemType } from '../menu-item-type.model'; const expectedSections: PartialMenuSection[] = [ diff --git a/src/app/shared/menu/providers/curation.menu.spec.ts b/src/app/shared/menu/providers/curation.menu.spec.ts index 5225091f75..7cfe9ef8e7 100644 --- a/src/app/shared/menu/providers/curation.menu.spec.ts +++ b/src/app/shared/menu/providers/curation.menu.spec.ts @@ -7,7 +7,7 @@ */ import { TestBed } from '@angular/core/testing'; -import { PartialMenuSection } from '../menu-provider'; +import { PartialMenuSection } from '../menu-provider.model'; import { MenuItemType } from '../menu-item-type.model'; import { AuthorizationDataServiceStub } from '../../testing/authorization-service.stub'; import { of as observableOf } from 'rxjs'; diff --git a/src/app/shared/menu/providers/edit.menu.spec.ts b/src/app/shared/menu/providers/edit.menu.spec.ts index b3204cc818..f47010d734 100644 --- a/src/app/shared/menu/providers/edit.menu.spec.ts +++ b/src/app/shared/menu/providers/edit.menu.spec.ts @@ -8,14 +8,15 @@ import { TestBed } from '@angular/core/testing'; import { MenuItemType } from '../menu-item-type.model'; -import { MenuSubSection, MenuTopSection } from './expandable-menu-provider'; +import { PartialMenuSection } from '../menu-provider.model'; import { AuthorizationDataServiceStub } from '../../testing/authorization-service.stub'; import { of as observableOf } from 'rxjs'; import { AuthorizationDataService } from '../../../core/data/feature-authorization/authorization-data.service'; import { FeatureID } from '../../../core/data/feature-authorization/feature-id'; import { EditMenuProvider } from './edit.menu'; -const expectedTopSection: MenuTopSection = { +const expectedTopSection: PartialMenuSection = { + visible: true, model: { type: MenuItemType.TEXT, text: 'menu.section.edit' @@ -23,7 +24,7 @@ const expectedTopSection: MenuTopSection = { icon: 'pencil', }; -const expectedSubSections: MenuSubSection[] = [ +const expectedSubSections: PartialMenuSection[] = [ { visible: true, model: { diff --git a/src/app/shared/menu/providers/export.menu.spec.ts b/src/app/shared/menu/providers/export.menu.spec.ts index 662622b594..72dfbe8167 100644 --- a/src/app/shared/menu/providers/export.menu.spec.ts +++ b/src/app/shared/menu/providers/export.menu.spec.ts @@ -8,7 +8,7 @@ import { TestBed } from '@angular/core/testing'; import { MenuItemType } from '../menu-item-type.model'; -import { MenuSubSection, MenuTopSection } from './expandable-menu-provider'; +import { PartialMenuSection } from '../menu-provider.model'; import { AuthorizationDataServiceStub } from '../../testing/authorization-service.stub'; import { of as observableOf } from 'rxjs'; import { AuthorizationDataService } from '../../../core/data/feature-authorization/authorization-data.service'; @@ -16,16 +16,16 @@ import { ScriptDataService } from '../../../core/data/processes/script-data.serv import { ScriptServiceStub } from '../../testing/script-service.stub'; import { ExportMenuProvider } from './export.menu'; -const expectedTopSection: MenuTopSection = { +const expectedTopSection: PartialMenuSection = { + visible: true, model: { type: MenuItemType.TEXT, text: 'menu.section.export', }, icon: 'file-export', - shouldPersistOnRouteChange: true, }; -const expectedSubSections: MenuSubSection[] = [ +const expectedSubSections: PartialMenuSection[] = [ { visible: true, model: { @@ -33,7 +33,6 @@ const expectedSubSections: MenuSubSection[] = [ text: 'menu.section.export_metadata', function: jasmine.any(Function) as any, }, - shouldPersistOnRouteChange: true, }, { visible: true, @@ -42,7 +41,6 @@ const expectedSubSections: MenuSubSection[] = [ text: 'menu.section.export_batch', function: jasmine.any(Function) as any, }, - shouldPersistOnRouteChange: true, } ]; diff --git a/src/app/shared/menu/providers/health.menu.spec.ts b/src/app/shared/menu/providers/health.menu.spec.ts index 6e10e3cc5f..ee0a649c10 100644 --- a/src/app/shared/menu/providers/health.menu.spec.ts +++ b/src/app/shared/menu/providers/health.menu.spec.ts @@ -7,7 +7,7 @@ */ import { TestBed } from '@angular/core/testing'; -import { PartialMenuSection } from '../menu-provider'; +import { PartialMenuSection } from '../menu-provider.model'; import { MenuItemType } from '../menu-item-type.model'; import { AuthorizationDataServiceStub } from '../../testing/authorization-service.stub'; import { of as observableOf } from 'rxjs'; diff --git a/src/app/shared/menu/providers/import.menu.spec.ts b/src/app/shared/menu/providers/import.menu.spec.ts index 08223f31d7..71e6a89325 100644 --- a/src/app/shared/menu/providers/import.menu.spec.ts +++ b/src/app/shared/menu/providers/import.menu.spec.ts @@ -8,7 +8,7 @@ import { TestBed } from '@angular/core/testing'; import { MenuItemType } from '../menu-item-type.model'; -import { MenuSubSection, MenuTopSection } from './expandable-menu-provider'; +import { PartialMenuSection } from '../menu-provider.model'; import { AuthorizationDataServiceStub } from '../../testing/authorization-service.stub'; import { of as observableOf } from 'rxjs'; import { AuthorizationDataService } from '../../../core/data/feature-authorization/authorization-data.service'; @@ -16,16 +16,16 @@ import { ImportMenuProvider } from './import.menu'; import { ScriptDataService } from '../../../core/data/processes/script-data.service'; import { ScriptServiceStub } from '../../testing/script-service.stub'; -const expectedTopSection: MenuTopSection = { +const expectedTopSection: PartialMenuSection = { + visible: true, model: { type: MenuItemType.TEXT, text: 'menu.section.import', }, icon: 'file-import', - shouldPersistOnRouteChange: true, }; -const expectedSubSections: MenuSubSection[] = [ +const expectedSubSections: PartialMenuSection[] = [ { visible: true, model: { diff --git a/src/app/shared/menu/providers/new.menu.spec.ts b/src/app/shared/menu/providers/new.menu.spec.ts index 6376b5e2d3..f98362ea8e 100644 --- a/src/app/shared/menu/providers/new.menu.spec.ts +++ b/src/app/shared/menu/providers/new.menu.spec.ts @@ -8,14 +8,15 @@ import { TestBed } from '@angular/core/testing'; import { MenuItemType } from '../menu-item-type.model'; -import { MenuSubSection, MenuTopSection } from './expandable-menu-provider'; +import { PartialMenuSection } from '../menu-provider.model'; import { NewMenuProvider } from './new.menu'; import { AuthorizationDataServiceStub } from '../../testing/authorization-service.stub'; import { of as observableOf } from 'rxjs'; import { AuthorizationDataService } from '../../../core/data/feature-authorization/authorization-data.service'; import { FeatureID } from '../../../core/data/feature-authorization/feature-id'; -const expectedTopSection: MenuTopSection = { +const expectedTopSection: PartialMenuSection = { + visible: true, model: { type: MenuItemType.TEXT, text: 'menu.section.new' @@ -23,7 +24,7 @@ const expectedTopSection: MenuTopSection = { icon: 'plus', }; -const expectedSubSections: MenuSubSection[] = [ +const expectedSubSections: PartialMenuSection[] = [ { visible: true, model: { diff --git a/src/app/shared/menu/providers/processes.menu.spec.ts b/src/app/shared/menu/providers/processes.menu.spec.ts index 78ff544a58..cef2f695eb 100644 --- a/src/app/shared/menu/providers/processes.menu.spec.ts +++ b/src/app/shared/menu/providers/processes.menu.spec.ts @@ -7,7 +7,7 @@ */ import { TestBed } from '@angular/core/testing'; -import { PartialMenuSection } from '../menu-provider'; +import { PartialMenuSection } from '../menu-provider.model'; import { MenuItemType } from '../menu-item-type.model'; import { AuthorizationDataServiceStub } from '../../testing/authorization-service.stub'; import { of as observableOf } from 'rxjs'; diff --git a/src/app/shared/menu/providers/registries.menu.spec.ts b/src/app/shared/menu/providers/registries.menu.spec.ts index b1aa981f7f..ef8f468039 100644 --- a/src/app/shared/menu/providers/registries.menu.spec.ts +++ b/src/app/shared/menu/providers/registries.menu.spec.ts @@ -8,7 +8,7 @@ import { TestBed } from '@angular/core/testing'; import { MenuItemType } from '../menu-item-type.model'; -import { MenuSubSection, MenuTopSection } from './expandable-menu-provider'; +import { PartialMenuSection } from '../menu-provider.model'; import { AuthorizationDataServiceStub } from '../../testing/authorization-service.stub'; import { of as observableOf } from 'rxjs'; import { AuthorizationDataService } from '../../../core/data/feature-authorization/authorization-data.service'; @@ -16,7 +16,8 @@ import { ScriptDataService } from '../../../core/data/processes/script-data.serv import { ScriptServiceStub } from '../../testing/script-service.stub'; import { RegistriesMenuProvider } from './registries.menu'; -const expectedTopSection: MenuTopSection = { +const expectedTopSection: PartialMenuSection = { + visible: true, model: { type: MenuItemType.TEXT, text: 'menu.section.registries', @@ -24,7 +25,7 @@ const expectedTopSection: MenuTopSection = { icon: 'list', }; -const expectedSubSections: MenuSubSection[] = [ +const expectedSubSections: PartialMenuSection[] = [ { visible: true, model: { diff --git a/src/app/shared/menu/providers/system-wide-alert.menu.spec.ts b/src/app/shared/menu/providers/system-wide-alert.menu.spec.ts index df5e126814..1cda898373 100644 --- a/src/app/shared/menu/providers/system-wide-alert.menu.spec.ts +++ b/src/app/shared/menu/providers/system-wide-alert.menu.spec.ts @@ -7,7 +7,7 @@ */ import { TestBed } from '@angular/core/testing'; -import { PartialMenuSection } from '../menu-provider'; +import { PartialMenuSection } from '../menu-provider.model'; import { MenuItemType } from '../menu-item-type.model'; import { AuthorizationDataServiceStub } from '../../testing/authorization-service.stub'; import { of as observableOf } from 'rxjs'; diff --git a/src/app/shared/menu/providers/workflow.menu.spec.ts b/src/app/shared/menu/providers/workflow.menu.spec.ts index 2886d0dbf3..5ed2af9d18 100644 --- a/src/app/shared/menu/providers/workflow.menu.spec.ts +++ b/src/app/shared/menu/providers/workflow.menu.spec.ts @@ -7,7 +7,7 @@ */ import { TestBed } from '@angular/core/testing'; -import { PartialMenuSection } from '../menu-provider'; +import { PartialMenuSection } from '../menu-provider.model'; import { MenuItemType } from '../menu-item-type.model'; import { AuthorizationDataServiceStub } from '../../testing/authorization-service.stub'; import { of as observableOf } from 'rxjs'; From 44d2450543efab8a220ee477e3a7bb0d1487f0e8 Mon Sep 17 00:00:00 2001 From: Alexandre Vryghem Date: Fri, 29 Nov 2024 15:15:51 +0100 Subject: [PATCH 09/32] 121550: Fixed dso edit menu crashing when dso isn't resolved yet --- src/app/shared/menu/providers/dso-option.menu.ts | 5 ----- src/app/shared/menu/providers/helper-providers/dso.menu.ts | 4 ++++ 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/src/app/shared/menu/providers/dso-option.menu.ts b/src/app/shared/menu/providers/dso-option.menu.ts index 3313f7ebca..0fba0e19ef 100644 --- a/src/app/shared/menu/providers/dso-option.menu.ts +++ b/src/app/shared/menu/providers/dso-option.menu.ts @@ -12,7 +12,6 @@ import { MenuItemType } from '../menu-item-type.model'; 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 @@ -23,10 +22,6 @@ export class DsoOptionMenu extends DSpaceObjectPageMenuProvider { alwaysRenderExpandable = true; - protected isApplicable(dso: DSpaceObject): boolean { - return hasValue(dso); - } - getSectionsForContext(dso: DSpaceObject): Observable { return of([ { 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 9b2d8642fa..4736e65f41 100644 --- a/src/app/shared/menu/providers/helper-providers/dso.menu.ts +++ b/src/app/shared/menu/providers/helper-providers/dso.menu.ts @@ -40,4 +40,8 @@ export abstract class DSpaceObjectPageMenuProvider extends AbstractRouteContextM return dso.type.toString().toLowerCase(); } } + + protected isApplicable(dso: DSpaceObject): boolean { + return hasValue(dso); + } } From d85d5fbd71bcf02c3a536e7fcab63e3873eb5c5a Mon Sep 17 00:00:00 2001 From: Alexandre Vryghem Date: Fri, 13 Dec 2024 12:25:39 +0100 Subject: [PATCH 10/32] 121550: Use IDs on the routes in order to assign AbstractRouteContextMenuProviders --- src/app/app.menus.ts | 32 ++++++++++++++----- .../collection-page-routing.module.ts | 4 +++ .../community-page-routing.module.ts | 4 +++ src/app/item-page/item-page-routing.module.ts | 7 ++++ src/app/shared/menu/menu-provider.model.ts | 7 ++-- src/app/shared/menu/menu-provider.service.ts | 5 +-- src/app/shared/menu/menu-route.model.ts | 9 ++++++ src/app/shared/menu/menu.structure.ts | 3 +- 8 files changed, 57 insertions(+), 14 deletions(-) create mode 100644 src/app/shared/menu/menu-route.model.ts diff --git a/src/app/app.menus.ts b/src/app/app.menus.ts index 7679db4c42..e530cfd55d 100644 --- a/src/app/app.menus.ts +++ b/src/app/app.menus.ts @@ -27,10 +27,8 @@ import { RegistriesMenuProvider } from './shared/menu/providers/registries.menu' import { StatisticsMenuProvider } from './shared/menu/providers/statistics.menu'; import { SystemWideAlertMenuProvider } from './shared/menu/providers/system-wide-alert.menu'; import { WorkflowMenuProvider } from './shared/menu/providers/workflow.menu'; -import { COMMUNITY_MODULE_PATH } from './community-page/community-page-routing-paths'; -import { COLLECTION_MODULE_PATH } from './collection-page/collection-page-routing-paths'; -import { ENTITY_MODULE_PATH, ITEM_MODULE_PATH } from './item-page/item-page-routing-paths'; import { DsoOptionMenu } from './shared/menu/providers/dso-option.menu'; +import { MenuRoute } from './shared/menu/menu-route.model'; export const MENUS = buildMenuStructure({ [MenuID.PUBLIC]: [ @@ -54,11 +52,29 @@ export const MENUS = buildMenuStructure({ ], [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), + SubscribeMenuProvider.onRoute( + MenuRoute.SIMPLE_COMMUNITY_PAGE, + MenuRoute.SIMPLE_COLLECTION_PAGE, + ), + DSpaceObjectEditMenuProvider.onRoute( + MenuRoute.SIMPLE_COMMUNITY_PAGE, + MenuRoute.SIMPLE_COLLECTION_PAGE, + MenuRoute.SIMPLE_ITEM_PAGE, + MenuRoute.FULL_ITEM_PAGE, + ), + VersioningMenuProvider.onRoute( + MenuRoute.SIMPLE_ITEM_PAGE, + MenuRoute.FULL_ITEM_PAGE, + ), + OrcidMenuProvider.onRoute( + MenuRoute.SIMPLE_ITEM_PAGE, + MenuRoute.FULL_ITEM_PAGE, + ), + ClaimMenuProvider.onRoute( + MenuRoute.SIMPLE_ITEM_PAGE, + MenuRoute.FULL_ITEM_PAGE, + MenuRoute.SIMPLE_COLLECTION_PAGE, + ), ]), ], }); diff --git a/src/app/collection-page/collection-page-routing.module.ts b/src/app/collection-page/collection-page-routing.module.ts index 7930c1ae6f..51d70ac8f1 100644 --- a/src/app/collection-page/collection-page-routing.module.ts +++ b/src/app/collection-page/collection-page-routing.module.ts @@ -19,6 +19,7 @@ import { DeleteCollectionPageComponent } from './delete-collection-page/delete-c import { ItemTemplatePageResolver } from './edit-item-template-page/item-template-page.resolver'; import { ThemedEditItemTemplatePageComponent } from './edit-item-template-page/themed-edit-item-template-page.component'; import { ThemedCollectionPageComponent } from './themed-collection-page.component'; +import { MenuRoute } from '../shared/menu/menu-route.model'; @NgModule({ imports: [ @@ -62,6 +63,9 @@ import { ThemedCollectionPageComponent } from './themed-collection-page.componen path: '', component: ThemedCollectionPageComponent, pathMatch: 'full', + data: { + menuRoute: MenuRoute.SIMPLE_COLLECTION_PAGE, + }, } ], }, diff --git a/src/app/community-page/community-page-routing.module.ts b/src/app/community-page/community-page-routing.module.ts index 006493c02c..e05314ce54 100644 --- a/src/app/community-page/community-page-routing.module.ts +++ b/src/app/community-page/community-page-routing.module.ts @@ -15,6 +15,7 @@ import { CreateCommunityPageComponent } from './create-community-page/create-com import { CreateCommunityPageGuard } from './create-community-page/create-community-page.guard'; import { DeleteCommunityPageComponent } from './delete-community-page/delete-community-page.component'; import { ThemedCommunityPageComponent } from './themed-community-page.component'; +import { MenuRoute } from '../shared/menu/menu-route.model'; @NgModule({ imports: [ @@ -48,6 +49,9 @@ import { ThemedCommunityPageComponent } from './themed-community-page.component' path: '', component: ThemedCommunityPageComponent, pathMatch: 'full', + data: { + menuRoute: MenuRoute.SIMPLE_COMMUNITY_PAGE, + }, } ], }, diff --git a/src/app/item-page/item-page-routing.module.ts b/src/app/item-page/item-page-routing.module.ts index 1c93dab744..2b11c42381 100644 --- a/src/app/item-page/item-page-routing.module.ts +++ b/src/app/item-page/item-page-routing.module.ts @@ -18,6 +18,7 @@ import { OrcidPageGuard } from './orcid-page/orcid-page.guard'; import { ThemedItemPageComponent } from './simple/themed-item-page.component'; import { VersionPageComponent } from './version-page/version-page/version-page.component'; import { VersionResolver } from './version-page/version.resolver'; +import { MenuRoute } from '../shared/menu/menu-route.model'; @NgModule({ imports: [ @@ -34,10 +35,16 @@ import { VersionResolver } from './version-page/version.resolver'; path: '', component: ThemedItemPageComponent, pathMatch: 'full', + data: { + menuRoute: MenuRoute.SIMPLE_ITEM_PAGE, + }, }, { path: 'full', component: ThemedFullItemPageComponent, + data: { + menuRoute: MenuRoute.FULL_ITEM_PAGE, + }, }, { path: ITEM_EDIT_PATH, diff --git a/src/app/shared/menu/menu-provider.model.ts b/src/app/shared/menu/menu-provider.model.ts index 247963f172..8a517934b9 100644 --- a/src/app/shared/menu/menu-provider.model.ts +++ b/src/app/shared/menu/menu-provider.model.ts @@ -11,6 +11,7 @@ import { Observable, } from 'rxjs'; import { MenuID } from './menu-id.model'; import { MenuItemModels } from './menu-section.model'; import { Type } from '@angular/core'; +import { MenuRoute } from './menu-route.model'; /** * Partial menu section @@ -52,7 +53,7 @@ export interface MenuProvider { */ export class MenuProviderTypeWithOptions { providerType: Type; - paths?: string[]; + paths?: MenuRoute[]; childProviderTypes?: (Type | MenuProviderTypeWithOptions)[]; } @@ -90,7 +91,7 @@ export abstract class AbstractMenuProvider implements MenuProvider { * 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[]; + activePaths?: MenuRoute[]; /** * The ID of the parent provider of this provider. @@ -110,7 +111,7 @@ export abstract class AbstractMenuProvider implements MenuProvider { * 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 { + public static onRoute(...paths: MenuRoute[]): MenuProviderTypeWithOptions { if (!AbstractMenuProvider.isPrototypeOf(this)) { throw new Error( 'onRoute should only be called from concrete subclasses of AbstractMenuProvider' diff --git a/src/app/shared/menu/menu-provider.service.ts b/src/app/shared/menu/menu-provider.service.ts index 9b5d5e73a5..c0f34be481 100644 --- a/src/app/shared/menu/menu-provider.service.ts +++ b/src/app/shared/menu/menu-provider.service.ts @@ -16,6 +16,7 @@ import { AbstractMenuProvider, PartialMenuSection } from './menu-provider.model' import { MenuState } from './menu-state.model'; import { MenuService } from './menu.service'; import { MENU_PROVIDER } from './menu.structure'; +import { MenuRoute } from './menu-route.model'; /** * Service that is responsible for adding and removing the menu sections created by the providers, both for @@ -126,8 +127,8 @@ export class MenuProviderService { .filter(provider => { let shouldUpdate = false; if (!provider.shouldPersistOnRouteChange && isNotEmpty(provider.activePaths)) { - provider.activePaths.forEach((path) => { - if (state.url.includes(path)) { + provider.activePaths.forEach((path: MenuRoute) => { + if (route.data.menuRoute === path) { shouldUpdate = true; } }); diff --git a/src/app/shared/menu/menu-route.model.ts b/src/app/shared/menu/menu-route.model.ts new file mode 100644 index 0000000000..db0fe03169 --- /dev/null +++ b/src/app/shared/menu/menu-route.model.ts @@ -0,0 +1,9 @@ +/** + * The menu route IDs that can be used for route resolvers + */ +export enum MenuRoute { + SIMPLE_COMMUNITY_PAGE = 'simple-community-page', + SIMPLE_COLLECTION_PAGE = 'simple-collection-page', + SIMPLE_ITEM_PAGE = 'simple-item-page', + FULL_ITEM_PAGE = 'full-item-page', +} diff --git a/src/app/shared/menu/menu.structure.ts b/src/app/shared/menu/menu.structure.ts index 581fff6c9a..81103ce182 100644 --- a/src/app/shared/menu/menu.structure.ts +++ b/src/app/shared/menu/menu.structure.ts @@ -10,6 +10,7 @@ import { MenuID } from './menu-id.model'; import { AbstractMenuProvider, MenuProviderTypeWithOptions } from './menu-provider.model'; import { MenuProviderService } from './menu-provider.service'; import { hasValue, isNotEmpty } from '../empty.util'; +import { MenuRoute } from './menu-route.model'; export const MENU_PROVIDER = new InjectionToken('MENU_PROVIDER'); @@ -75,7 +76,7 @@ function processProviderType(providers: Provider[], menuID: string, providerType * @param hasSubProviders - Whether this provider has sub providers * @param paths - The paths this provider should be active on if relevant */ -function addProviderToList(providers: Provider[], providerType: Type, menuID: string, index: number, parentID?: string, hasSubProviders?: boolean, paths?: string[]) { +function addProviderToList(providers: Provider[], providerType: Type, menuID: string, index: number, parentID?: string, hasSubProviders?: boolean, paths?: MenuRoute[]) { const resolvedProvider = { provide: MENU_PROVIDER, multi: true, From 4729ae950d8821e1193e076c11d1c0e30503d027 Mon Sep 17 00:00:00 2001 From: Yana De Pauw Date: Thu, 2 Jan 2025 14:17:06 +0100 Subject: [PATCH 11/32] Fix merge issues --- .../shared/menu/providers/access-control.menu.spec.ts | 7 ++++--- .../shared/menu/providers/admin-search.menu.spec.ts | 2 +- src/app/shared/menu/providers/browse.menu.spec.ts | 9 +++++---- .../shared/menu/providers/community-list.menu.spec.ts | 2 +- src/app/shared/menu/providers/curation.menu.spec.ts | 2 +- src/app/shared/menu/providers/edit.menu.spec.ts | 7 ++++--- src/app/shared/menu/providers/export.menu.spec.ts | 10 ++++------ src/app/shared/menu/providers/health.menu.spec.ts | 2 +- src/app/shared/menu/providers/import.menu.spec.ts | 8 ++++---- src/app/shared/menu/providers/new.menu.spec.ts | 9 +++++---- src/app/shared/menu/providers/processes.menu.spec.ts | 2 +- src/app/shared/menu/providers/registries.menu.spec.ts | 7 ++++--- .../menu/providers/system-wide-alert.menu.spec.ts | 2 +- src/app/shared/menu/providers/workflow.menu.spec.ts | 2 +- 14 files changed, 37 insertions(+), 34 deletions(-) diff --git a/src/app/shared/menu/providers/access-control.menu.spec.ts b/src/app/shared/menu/providers/access-control.menu.spec.ts index d9284a75c4..dfb3b625e1 100644 --- a/src/app/shared/menu/providers/access-control.menu.spec.ts +++ b/src/app/shared/menu/providers/access-control.menu.spec.ts @@ -8,7 +8,6 @@ import { TestBed } from '@angular/core/testing'; import { MenuItemType } from '../menu-item-type.model'; -import { MenuSubSection, MenuTopSection } from './expandable-menu-provider'; import { AuthorizationDataServiceStub } from '../../testing/authorization-service.stub'; import { of as observableOf } from 'rxjs'; import { AuthorizationDataService } from '../../../core/data/feature-authorization/authorization-data.service'; @@ -16,8 +15,10 @@ import { FeatureID } from '../../../core/data/feature-authorization/feature-id'; import { AccessControlMenuProvider } from './access-control.menu'; import { ScriptDataService } from '../../../core/data/processes/script-data.service'; import { ScriptServiceStub } from '../../testing/script-service.stub'; +import { PartialMenuSection } from '../menu-provider.model'; -const expectedTopSection: MenuTopSection = { +const expectedTopSection: PartialMenuSection = { + visible: true, model: { type: MenuItemType.TEXT, text: 'menu.section.access_control', @@ -25,7 +26,7 @@ const expectedTopSection: MenuTopSection = { icon: 'key' }; -const expectedSubSections: MenuSubSection[] = [ +const expectedSubSections: PartialMenuSection[] = [ { visible: true, model: { diff --git a/src/app/shared/menu/providers/admin-search.menu.spec.ts b/src/app/shared/menu/providers/admin-search.menu.spec.ts index 0471215578..ffd51425d7 100644 --- a/src/app/shared/menu/providers/admin-search.menu.spec.ts +++ b/src/app/shared/menu/providers/admin-search.menu.spec.ts @@ -7,12 +7,12 @@ */ import { TestBed } from '@angular/core/testing'; -import { PartialMenuSection } from '../menu-provider'; import { MenuItemType } from '../menu-item-type.model'; import { AdminSearchMenuProvider } from './admin-search.menu'; import { AuthorizationDataServiceStub } from '../../testing/authorization-service.stub'; import { of as observableOf } from 'rxjs'; import { AuthorizationDataService } from '../../../core/data/feature-authorization/authorization-data.service'; +import { PartialMenuSection } from '../menu-provider.model'; const expectedSections: PartialMenuSection[] = [ { diff --git a/src/app/shared/menu/providers/browse.menu.spec.ts b/src/app/shared/menu/providers/browse.menu.spec.ts index 20ac0f92be..546d598183 100644 --- a/src/app/shared/menu/providers/browse.menu.spec.ts +++ b/src/app/shared/menu/providers/browse.menu.spec.ts @@ -9,7 +9,6 @@ import { TestBed } from '@angular/core/testing'; import { BrowseMenuProvider } from './browse.menu'; import { MenuItemType } from '../menu-item-type.model'; -import { MenuSubSection, MenuTopSection } from './expandable-menu-provider'; import { BrowseService } from '../../../core/browse/browse.service'; import { BrowseServiceStub } from '../../testing/browse-service.stub'; import { ObjectCacheService } from '../../../core/cache/object-cache.service'; @@ -17,16 +16,18 @@ import { getMockObjectCacheService } from '../../mocks/object-cache.service.mock import { BrowseDefinition } from '../../../core/shared/browse-definition.model'; import { createSuccessfulRemoteDataObject$ } from '../../remote-data.utils'; import { createPaginatedList } from '../../testing/utils.test'; +import { PartialMenuSection } from '../menu-provider.model'; -const expectedTopSection: MenuTopSection = { - model: { +const expectedTopSection: PartialMenuSection = { + visible: true, + model: { type: MenuItemType.TEXT, text: 'menu.section.browse_global', }, icon: 'globe', }; -const expectedSubSections: MenuSubSection[] = [ +const expectedSubSections: PartialMenuSection[] = [ { visible: true, model: { diff --git a/src/app/shared/menu/providers/community-list.menu.spec.ts b/src/app/shared/menu/providers/community-list.menu.spec.ts index b0e037d228..ef5bd81f2a 100644 --- a/src/app/shared/menu/providers/community-list.menu.spec.ts +++ b/src/app/shared/menu/providers/community-list.menu.spec.ts @@ -8,8 +8,8 @@ import { TestBed } from '@angular/core/testing'; import { CommunityListMenuProvider } from './community-list.menu'; -import { PartialMenuSection } from '../menu-provider'; import { MenuItemType } from '../menu-item-type.model'; +import { PartialMenuSection } from '../menu-provider.model'; const expectedSections: PartialMenuSection[] = [ { diff --git a/src/app/shared/menu/providers/curation.menu.spec.ts b/src/app/shared/menu/providers/curation.menu.spec.ts index 5225091f75..d3e92fbc00 100644 --- a/src/app/shared/menu/providers/curation.menu.spec.ts +++ b/src/app/shared/menu/providers/curation.menu.spec.ts @@ -7,12 +7,12 @@ */ import { TestBed } from '@angular/core/testing'; -import { PartialMenuSection } from '../menu-provider'; import { MenuItemType } from '../menu-item-type.model'; import { AuthorizationDataServiceStub } from '../../testing/authorization-service.stub'; import { of as observableOf } from 'rxjs'; import { AuthorizationDataService } from '../../../core/data/feature-authorization/authorization-data.service'; import { CurationMenuProvider } from './curation.menu'; +import { PartialMenuSection } from '../menu-provider.model'; const expectedSections: PartialMenuSection[] = [ { diff --git a/src/app/shared/menu/providers/edit.menu.spec.ts b/src/app/shared/menu/providers/edit.menu.spec.ts index b3204cc818..570e2c4e9e 100644 --- a/src/app/shared/menu/providers/edit.menu.spec.ts +++ b/src/app/shared/menu/providers/edit.menu.spec.ts @@ -8,14 +8,15 @@ import { TestBed } from '@angular/core/testing'; import { MenuItemType } from '../menu-item-type.model'; -import { MenuSubSection, MenuTopSection } from './expandable-menu-provider'; import { AuthorizationDataServiceStub } from '../../testing/authorization-service.stub'; import { of as observableOf } from 'rxjs'; import { AuthorizationDataService } from '../../../core/data/feature-authorization/authorization-data.service'; import { FeatureID } from '../../../core/data/feature-authorization/feature-id'; import { EditMenuProvider } from './edit.menu'; +import { PartialMenuSection } from '../menu-provider.model'; -const expectedTopSection: MenuTopSection = { +const expectedTopSection: PartialMenuSection = { + visible: true, model: { type: MenuItemType.TEXT, text: 'menu.section.edit' @@ -23,7 +24,7 @@ const expectedTopSection: MenuTopSection = { icon: 'pencil', }; -const expectedSubSections: MenuSubSection[] = [ +const expectedSubSections: PartialMenuSection[] = [ { visible: true, model: { diff --git a/src/app/shared/menu/providers/export.menu.spec.ts b/src/app/shared/menu/providers/export.menu.spec.ts index 662622b594..7e46d2811b 100644 --- a/src/app/shared/menu/providers/export.menu.spec.ts +++ b/src/app/shared/menu/providers/export.menu.spec.ts @@ -8,24 +8,24 @@ import { TestBed } from '@angular/core/testing'; import { MenuItemType } from '../menu-item-type.model'; -import { MenuSubSection, MenuTopSection } from './expandable-menu-provider'; import { AuthorizationDataServiceStub } from '../../testing/authorization-service.stub'; import { of as observableOf } from 'rxjs'; import { AuthorizationDataService } from '../../../core/data/feature-authorization/authorization-data.service'; import { ScriptDataService } from '../../../core/data/processes/script-data.service'; import { ScriptServiceStub } from '../../testing/script-service.stub'; import { ExportMenuProvider } from './export.menu'; +import { PartialMenuSection } from '../menu-provider.model'; -const expectedTopSection: MenuTopSection = { +const expectedTopSection: PartialMenuSection = { + visible: true, model: { type: MenuItemType.TEXT, text: 'menu.section.export', }, icon: 'file-export', - shouldPersistOnRouteChange: true, }; -const expectedSubSections: MenuSubSection[] = [ +const expectedSubSections: PartialMenuSection[] = [ { visible: true, model: { @@ -33,7 +33,6 @@ const expectedSubSections: MenuSubSection[] = [ text: 'menu.section.export_metadata', function: jasmine.any(Function) as any, }, - shouldPersistOnRouteChange: true, }, { visible: true, @@ -42,7 +41,6 @@ const expectedSubSections: MenuSubSection[] = [ text: 'menu.section.export_batch', function: jasmine.any(Function) as any, }, - shouldPersistOnRouteChange: true, } ]; diff --git a/src/app/shared/menu/providers/health.menu.spec.ts b/src/app/shared/menu/providers/health.menu.spec.ts index 6e10e3cc5f..692fd2277c 100644 --- a/src/app/shared/menu/providers/health.menu.spec.ts +++ b/src/app/shared/menu/providers/health.menu.spec.ts @@ -7,12 +7,12 @@ */ import { TestBed } from '@angular/core/testing'; -import { PartialMenuSection } from '../menu-provider'; import { MenuItemType } from '../menu-item-type.model'; import { AuthorizationDataServiceStub } from '../../testing/authorization-service.stub'; import { of as observableOf } from 'rxjs'; import { AuthorizationDataService } from '../../../core/data/feature-authorization/authorization-data.service'; import { HealthMenuProvider } from './health.menu'; +import { PartialMenuSection } from '../menu-provider.model'; const expectedSections: PartialMenuSection[] = [ { diff --git a/src/app/shared/menu/providers/import.menu.spec.ts b/src/app/shared/menu/providers/import.menu.spec.ts index 08223f31d7..183dead399 100644 --- a/src/app/shared/menu/providers/import.menu.spec.ts +++ b/src/app/shared/menu/providers/import.menu.spec.ts @@ -8,24 +8,24 @@ import { TestBed } from '@angular/core/testing'; import { MenuItemType } from '../menu-item-type.model'; -import { MenuSubSection, MenuTopSection } from './expandable-menu-provider'; import { AuthorizationDataServiceStub } from '../../testing/authorization-service.stub'; import { of as observableOf } from 'rxjs'; import { AuthorizationDataService } from '../../../core/data/feature-authorization/authorization-data.service'; import { ImportMenuProvider } from './import.menu'; import { ScriptDataService } from '../../../core/data/processes/script-data.service'; import { ScriptServiceStub } from '../../testing/script-service.stub'; +import { PartialMenuSection } from '../menu-provider.model'; -const expectedTopSection: MenuTopSection = { +const expectedTopSection: PartialMenuSection = { + visible: true, model: { type: MenuItemType.TEXT, text: 'menu.section.import', }, icon: 'file-import', - shouldPersistOnRouteChange: true, }; -const expectedSubSections: MenuSubSection[] = [ +const expectedSubSections: PartialMenuSection[] = [ { visible: true, model: { diff --git a/src/app/shared/menu/providers/new.menu.spec.ts b/src/app/shared/menu/providers/new.menu.spec.ts index 6376b5e2d3..c03f788275 100644 --- a/src/app/shared/menu/providers/new.menu.spec.ts +++ b/src/app/shared/menu/providers/new.menu.spec.ts @@ -8,22 +8,23 @@ import { TestBed } from '@angular/core/testing'; import { MenuItemType } from '../menu-item-type.model'; -import { MenuSubSection, MenuTopSection } from './expandable-menu-provider'; import { NewMenuProvider } from './new.menu'; import { AuthorizationDataServiceStub } from '../../testing/authorization-service.stub'; import { of as observableOf } from 'rxjs'; import { AuthorizationDataService } from '../../../core/data/feature-authorization/authorization-data.service'; import { FeatureID } from '../../../core/data/feature-authorization/feature-id'; +import { PartialMenuSection } from '../menu-provider.model'; -const expectedTopSection: MenuTopSection = { - model: { +const expectedTopSection: PartialMenuSection = { + visible: true, + model: { type: MenuItemType.TEXT, text: 'menu.section.new' }, icon: 'plus', }; -const expectedSubSections: MenuSubSection[] = [ +const expectedSubSections: PartialMenuSection[] = [ { visible: true, model: { diff --git a/src/app/shared/menu/providers/processes.menu.spec.ts b/src/app/shared/menu/providers/processes.menu.spec.ts index 78ff544a58..eb72a02765 100644 --- a/src/app/shared/menu/providers/processes.menu.spec.ts +++ b/src/app/shared/menu/providers/processes.menu.spec.ts @@ -7,12 +7,12 @@ */ import { TestBed } from '@angular/core/testing'; -import { PartialMenuSection } from '../menu-provider'; import { MenuItemType } from '../menu-item-type.model'; import { AuthorizationDataServiceStub } from '../../testing/authorization-service.stub'; import { of as observableOf } from 'rxjs'; import { AuthorizationDataService } from '../../../core/data/feature-authorization/authorization-data.service'; import { ProcessesMenuProvider } from './processes.menu'; +import { PartialMenuSection } from '../menu-provider.model'; const expectedSections: PartialMenuSection[] = [ { diff --git a/src/app/shared/menu/providers/registries.menu.spec.ts b/src/app/shared/menu/providers/registries.menu.spec.ts index b1aa981f7f..ae8c419163 100644 --- a/src/app/shared/menu/providers/registries.menu.spec.ts +++ b/src/app/shared/menu/providers/registries.menu.spec.ts @@ -8,15 +8,16 @@ import { TestBed } from '@angular/core/testing'; import { MenuItemType } from '../menu-item-type.model'; -import { MenuSubSection, MenuTopSection } from './expandable-menu-provider'; import { AuthorizationDataServiceStub } from '../../testing/authorization-service.stub'; import { of as observableOf } from 'rxjs'; import { AuthorizationDataService } from '../../../core/data/feature-authorization/authorization-data.service'; import { ScriptDataService } from '../../../core/data/processes/script-data.service'; import { ScriptServiceStub } from '../../testing/script-service.stub'; import { RegistriesMenuProvider } from './registries.menu'; +import { PartialMenuSection } from '../menu-provider.model'; -const expectedTopSection: MenuTopSection = { +const expectedTopSection: PartialMenuSection = { + visible: true, model: { type: MenuItemType.TEXT, text: 'menu.section.registries', @@ -24,7 +25,7 @@ const expectedTopSection: MenuTopSection = { icon: 'list', }; -const expectedSubSections: MenuSubSection[] = [ +const expectedSubSections: PartialMenuSection[] = [ { visible: true, model: { diff --git a/src/app/shared/menu/providers/system-wide-alert.menu.spec.ts b/src/app/shared/menu/providers/system-wide-alert.menu.spec.ts index df5e126814..dbed3f6f59 100644 --- a/src/app/shared/menu/providers/system-wide-alert.menu.spec.ts +++ b/src/app/shared/menu/providers/system-wide-alert.menu.spec.ts @@ -7,12 +7,12 @@ */ import { TestBed } from '@angular/core/testing'; -import { PartialMenuSection } from '../menu-provider'; import { MenuItemType } from '../menu-item-type.model'; import { AuthorizationDataServiceStub } from '../../testing/authorization-service.stub'; import { of as observableOf } from 'rxjs'; import { AuthorizationDataService } from '../../../core/data/feature-authorization/authorization-data.service'; import { SystemWideAlertMenuProvider } from './system-wide-alert.menu'; +import { PartialMenuSection } from '../menu-provider.model'; const expectedSections: PartialMenuSection[] = [ { diff --git a/src/app/shared/menu/providers/workflow.menu.spec.ts b/src/app/shared/menu/providers/workflow.menu.spec.ts index 2886d0dbf3..14b14db115 100644 --- a/src/app/shared/menu/providers/workflow.menu.spec.ts +++ b/src/app/shared/menu/providers/workflow.menu.spec.ts @@ -7,12 +7,12 @@ */ import { TestBed } from '@angular/core/testing'; -import { PartialMenuSection } from '../menu-provider'; import { MenuItemType } from '../menu-item-type.model'; import { AuthorizationDataServiceStub } from '../../testing/authorization-service.stub'; import { of as observableOf } from 'rxjs'; import { AuthorizationDataService } from '../../../core/data/feature-authorization/authorization-data.service'; import { WorkflowMenuProvider } from './workflow.menu'; +import { PartialMenuSection } from '../menu-provider.model'; const expectedSections: PartialMenuSection[] = [ { From cb4a7b31f08d156e1a7e4b25c4ecb43d818c4ef5 Mon Sep 17 00:00:00 2001 From: Yana De Pauw Date: Mon, 6 Jan 2025 14:38:19 +0100 Subject: [PATCH 12/32] Add missing provider tests --- src/app/app.menus.ts | 4 +- src/app/shared/menu/menu.structure.spec.ts | 6 +- .../providers/access-control.menu.spec.ts | 76 +++++----- .../menu/providers/admin-search.menu.spec.ts | 28 ++-- .../shared/menu/providers/browse.menu.spec.ts | 50 ++++--- .../providers/comcol-subscribe.menu.spec.ts | 64 ++++++++ .../providers/community-list.menu.spec.ts | 24 +-- .../menu/providers/curation.menu.spec.ts | 26 ++-- .../menu/providers/dso-edit.menu.spec.ts | 65 ++++++++ .../menu/providers/dso-option.menu.spec.ts | 53 +++++++ .../shared/menu/providers/dso-option.menu.ts | 2 +- .../shared/menu/providers/edit.menu.spec.ts | 75 +++++----- .../shared/menu/providers/export.menu.spec.ts | 60 ++++---- .../shared/menu/providers/health.menu.spec.ts | 26 ++-- .../helper-providers/dso.menu.spec.ts | 139 ++++++++++++++++++ .../expandable-menu-provider.spec.ts | 113 ++++++++++++++ .../route-context.menu.spec.ts | 96 ++++++++++++ .../helper-providers/route-context.menu.ts | 2 +- .../shared/menu/providers/import.menu.spec.ts | 60 ++++---- .../menu/providers/item-claim.menu.spec.ts | 134 +++++++++++++++++ .../shared/menu/providers/item-claim.menu.ts | 17 +-- .../menu/providers/item-orcid.menu.spec.ts | 94 ++++++++++++ .../shared/menu/providers/item-orcid.menu.ts | 14 +- .../providers/item-versioning.menu.spec.ts | 98 ++++++++++++ .../shared/menu/providers/new.menu.spec.ts | 70 ++++----- .../menu/providers/processes.menu.spec.ts | 27 ++-- .../menu/providers/registries.menu.spec.ts | 61 ++++---- .../menu/providers/statistics.menu.spec.ts | 127 ++++++++++++++++ .../shared/menu/providers/statistics.menu.ts | 2 +- .../providers/system-wide-alert.menu.spec.ts | 27 ++-- .../menu/providers/workflow.menu.spec.ts | 27 ++-- 31 files changed, 1329 insertions(+), 338 deletions(-) create mode 100644 src/app/shared/menu/providers/comcol-subscribe.menu.spec.ts create mode 100644 src/app/shared/menu/providers/dso-edit.menu.spec.ts create mode 100644 src/app/shared/menu/providers/dso-option.menu.spec.ts create mode 100644 src/app/shared/menu/providers/helper-providers/dso.menu.spec.ts create mode 100644 src/app/shared/menu/providers/helper-providers/expandable-menu-provider.spec.ts create mode 100644 src/app/shared/menu/providers/helper-providers/route-context.menu.spec.ts create mode 100644 src/app/shared/menu/providers/item-claim.menu.spec.ts create mode 100644 src/app/shared/menu/providers/item-orcid.menu.spec.ts create mode 100644 src/app/shared/menu/providers/item-versioning.menu.spec.ts create mode 100644 src/app/shared/menu/providers/statistics.menu.spec.ts diff --git a/src/app/app.menus.ts b/src/app/app.menus.ts index e530cfd55d..8240a9cb1b 100644 --- a/src/app/app.menus.ts +++ b/src/app/app.menus.ts @@ -27,7 +27,7 @@ import { RegistriesMenuProvider } from './shared/menu/providers/registries.menu' import { StatisticsMenuProvider } from './shared/menu/providers/statistics.menu'; import { SystemWideAlertMenuProvider } from './shared/menu/providers/system-wide-alert.menu'; import { WorkflowMenuProvider } from './shared/menu/providers/workflow.menu'; -import { DsoOptionMenu } from './shared/menu/providers/dso-option.menu'; +import { DsoOptionMenuProvider } from './shared/menu/providers/dso-option.menu'; import { MenuRoute } from './shared/menu/menu-route.model'; export const MENUS = buildMenuStructure({ @@ -51,7 +51,7 @@ export const MENUS = buildMenuStructure({ SystemWideAlertMenuProvider, ], [MenuID.DSO_EDIT]: [ - DsoOptionMenu.withSubs([ + DsoOptionMenuProvider.withSubs([ SubscribeMenuProvider.onRoute( MenuRoute.SIMPLE_COMMUNITY_PAGE, MenuRoute.SIMPLE_COLLECTION_PAGE, diff --git a/src/app/shared/menu/menu.structure.spec.ts b/src/app/shared/menu/menu.structure.spec.ts index a54622731b..f62ef03cd1 100644 --- a/src/app/shared/menu/menu.structure.spec.ts +++ b/src/app/shared/menu/menu.structure.spec.ts @@ -1,7 +1,7 @@ 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 { DsoOptionMenuProvider } from './providers/dso-option.menu'; import { SubscribeMenuProvider } from './providers/comcol-subscribe.menu'; import { buildMenuStructure } from './menu.structure'; import { MenuProviderService } from './menu-provider.service'; @@ -47,7 +47,7 @@ describe('buildMenuStructure', () => { SystemWideAlertMenuProvider, ], [MenuID.DSO_EDIT]: [ - DsoOptionMenu.withSubs([ + DsoOptionMenuProvider.withSubs([ SubscribeMenuProvider.onRoute( MenuRoute.SIMPLE_COMMUNITY_PAGE, MenuRoute.SIMPLE_COLLECTION_PAGE @@ -97,7 +97,7 @@ describe('buildMenuStructure', () => { VersioningMenuProvider, OrcidMenuProvider, ClaimMenuProvider, - DsoOptionMenu, + DsoOptionMenuProvider, ]; diff --git a/src/app/shared/menu/providers/access-control.menu.spec.ts b/src/app/shared/menu/providers/access-control.menu.spec.ts index dfb3b625e1..b55b851927 100644 --- a/src/app/shared/menu/providers/access-control.menu.spec.ts +++ b/src/app/shared/menu/providers/access-control.menu.spec.ts @@ -17,43 +17,43 @@ import { ScriptDataService } from '../../../core/data/processes/script-data.serv import { ScriptServiceStub } from '../../testing/script-service.stub'; import { PartialMenuSection } from '../menu-provider.model'; -const expectedTopSection: PartialMenuSection = { - visible: true, - model: { - type: MenuItemType.TEXT, - text: 'menu.section.access_control', - }, - icon: 'key' -}; - -const expectedSubSections: PartialMenuSection[] = [ - { - visible: true, - model: { - type: MenuItemType.LINK, - text: 'menu.section.access_control_people', - link: '/access-control/epeople', - }, - }, - { - visible: false, - model: { - type: MenuItemType.LINK, - text: 'menu.section.access_control_groups', - link: '/access-control/groups', - }, - }, - { - visible: true, - model: { - type: MenuItemType.LINK, - text: 'menu.section.access_control_bulk', - link: '/access-control/bulk-access', - }, - }, -]; - describe('AccessControlMenuProvider', () => { + const expectedTopSection: PartialMenuSection = { + visible: true, + model: { + type: MenuItemType.TEXT, + text: 'menu.section.access_control', + }, + icon: 'key' + }; + + const expectedSubSections: PartialMenuSection[] = [ + { + visible: true, + model: { + type: MenuItemType.LINK, + text: 'menu.section.access_control_people', + link: '/access-control/epeople', + }, + }, + { + visible: false, + model: { + type: MenuItemType.LINK, + text: 'menu.section.access_control_groups', + link: '/access-control/groups', + }, + }, + { + visible: true, + model: { + type: MenuItemType.LINK, + text: 'menu.section.access_control_bulk', + link: '/access-control/bulk-access', + }, + }, + ]; + let provider: AccessControlMenuProvider; let authorizationServiceStub = new AuthorizationDataServiceStub(); @@ -69,8 +69,8 @@ describe('AccessControlMenuProvider', () => { TestBed.configureTestingModule({ providers: [ AccessControlMenuProvider, - { provide: AuthorizationDataService, useValue: authorizationServiceStub }, - { provide: ScriptDataService, useClass: ScriptServiceStub }, + {provide: AuthorizationDataService, useValue: authorizationServiceStub}, + {provide: ScriptDataService, useClass: ScriptServiceStub}, ], }); provider = TestBed.inject(AccessControlMenuProvider); diff --git a/src/app/shared/menu/providers/admin-search.menu.spec.ts b/src/app/shared/menu/providers/admin-search.menu.spec.ts index ffd51425d7..b47b2354d4 100644 --- a/src/app/shared/menu/providers/admin-search.menu.spec.ts +++ b/src/app/shared/menu/providers/admin-search.menu.spec.ts @@ -14,19 +14,21 @@ import { of as observableOf } from 'rxjs'; import { AuthorizationDataService } from '../../../core/data/feature-authorization/authorization-data.service'; import { PartialMenuSection } from '../menu-provider.model'; -const expectedSections: PartialMenuSection[] = [ - { - visible: true, - model: { - type: MenuItemType.LINK, - text: 'menu.section.admin_search', - link: '/admin/search', - }, - icon: 'search', - }, -]; - describe('AdminSearchMenuProvider', () => { + + const expectedSections: PartialMenuSection[] = [ + { + visible: true, + model: { + type: MenuItemType.LINK, + text: 'menu.section.admin_search', + link: '/admin/search', + }, + icon: 'search', + }, + ]; + + let provider: AdminSearchMenuProvider; let authorizationServiceStub = new AuthorizationDataServiceStub(); @@ -38,7 +40,7 @@ describe('AdminSearchMenuProvider', () => { TestBed.configureTestingModule({ providers: [ AdminSearchMenuProvider, - { provide: AuthorizationDataService, useValue: authorizationServiceStub }, + {provide: AuthorizationDataService, useValue: authorizationServiceStub}, ], }); provider = TestBed.inject(AdminSearchMenuProvider); diff --git a/src/app/shared/menu/providers/browse.menu.spec.ts b/src/app/shared/menu/providers/browse.menu.spec.ts index 546d598183..0585db6d82 100644 --- a/src/app/shared/menu/providers/browse.menu.spec.ts +++ b/src/app/shared/menu/providers/browse.menu.spec.ts @@ -18,51 +18,53 @@ import { createSuccessfulRemoteDataObject$ } from '../../remote-data.utils'; import { createPaginatedList } from '../../testing/utils.test'; import { PartialMenuSection } from '../menu-provider.model'; -const expectedTopSection: PartialMenuSection = { - visible: true, - model: { +describe('BrowseMenuProvider', () => { + + const expectedTopSection: PartialMenuSection = { + visible: true, + model: { type: MenuItemType.TEXT, text: 'menu.section.browse_global', }, icon: 'globe', }; -const expectedSubSections: PartialMenuSection[] = [ - { - visible: true, - model: { - type: MenuItemType.LINK, - text: 'menu.section.browse_global_by_author', - link: '/browse/author', + const expectedSubSections: PartialMenuSection[] = [ + { + visible: true, + model: { + type: MenuItemType.LINK, + text: 'menu.section.browse_global_by_author', + link: '/browse/author', + }, }, - }, - { - visible: true, - model: { - type: MenuItemType.LINK, - text: 'menu.section.browse_global_by_subject', - link: '/browse/subject', + { + visible: true, + model: { + type: MenuItemType.LINK, + text: 'menu.section.browse_global_by_subject', + link: '/browse/subject', + }, }, - }, -]; + ]; + -describe('BrowseMenuProvider', () => { let provider: BrowseMenuProvider; let browseServiceStub = new BrowseServiceStub(); beforeEach(() => { spyOn(browseServiceStub, 'getBrowseDefinitions').and.returnValue( createSuccessfulRemoteDataObject$(createPaginatedList([ - { id: 'author' } as BrowseDefinition, - { id: 'subject' } as BrowseDefinition, + {id: 'author'} as BrowseDefinition, + {id: 'subject'} as BrowseDefinition, ])) ); TestBed.configureTestingModule({ providers: [ BrowseMenuProvider, - { provide: BrowseService, useValue: browseServiceStub }, - { provide: ObjectCacheService, useValue: getMockObjectCacheService() }, + {provide: BrowseService, useValue: browseServiceStub}, + {provide: ObjectCacheService, useValue: getMockObjectCacheService()}, ], }); provider = TestBed.inject(BrowseMenuProvider); diff --git a/src/app/shared/menu/providers/comcol-subscribe.menu.spec.ts b/src/app/shared/menu/providers/comcol-subscribe.menu.spec.ts new file mode 100644 index 0000000000..32d64cb748 --- /dev/null +++ b/src/app/shared/menu/providers/comcol-subscribe.menu.spec.ts @@ -0,0 +1,64 @@ +import { PartialMenuSection } from '../menu-provider.model'; +import { MenuItemType } from '../menu-item-type.model'; +import { TestBed } from '@angular/core/testing'; +import { of as observableOf } from 'rxjs'; +import { AuthorizationDataService } from '../../../core/data/feature-authorization/authorization-data.service'; +import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; +import { SubscribeMenuProvider } from './comcol-subscribe.menu'; +import { Collection } from '../../../core/shared/collection.model'; + +describe('SubscribeMenuProvider', () => { + + const expectedSections: PartialMenuSection[] = [ + { + visible: true, + model: { + type: MenuItemType.ONCLICK, + text: 'subscriptions.tooltip', + function: jasmine.any(Function) as any, + }, + icon: 'bell', + } + ]; + + let provider: SubscribeMenuProvider; + + const dso: Collection = Object.assign(new Collection(), {_links: {self: {href: 'self-link'}}}); + + + let authorizationService; + let modalService; + + beforeEach(() => { + + authorizationService = jasmine.createSpyObj('authorizationService', { + 'isAuthorized': observableOf(true) + }); + + modalService = jasmine.createSpyObj('modalService', ['open']); + + TestBed.configureTestingModule({ + providers: [ + SubscribeMenuProvider, + {provide: AuthorizationDataService, useValue: authorizationService}, + {provide: NgbModal, useValue: modalService}, + ], + }); + provider = TestBed.inject(SubscribeMenuProvider); + }); + + it('should be created', () => { + expect(provider).toBeTruthy(); + }); + + describe('getSectionsForContext', () => { + it('should return the expected sections', (done) => { + provider.getSectionsForContext(dso).subscribe((sections) => { + expect(sections).toEqual(expectedSections); + done(); + }); + }); + }); + + +}); diff --git a/src/app/shared/menu/providers/community-list.menu.spec.ts b/src/app/shared/menu/providers/community-list.menu.spec.ts index ef5bd81f2a..435e2d5396 100644 --- a/src/app/shared/menu/providers/community-list.menu.spec.ts +++ b/src/app/shared/menu/providers/community-list.menu.spec.ts @@ -11,19 +11,19 @@ import { CommunityListMenuProvider } from './community-list.menu'; import { MenuItemType } from '../menu-item-type.model'; import { PartialMenuSection } from '../menu-provider.model'; -const expectedSections: PartialMenuSection[] = [ - { - visible: true, - model: { - type: MenuItemType.LINK, - text: `menu.section.browse_global_communities_and_collections`, - link: `/community-list`, - }, - icon: 'diagram-project' - }, -]; - describe('CommunityListMenuProvider', () => { + const expectedSections: PartialMenuSection[] = [ + { + visible: true, + model: { + type: MenuItemType.LINK, + text: `menu.section.browse_global_communities_and_collections`, + link: `/community-list`, + }, + icon: 'diagram-project' + }, + ]; + let provider: CommunityListMenuProvider; beforeEach(() => { diff --git a/src/app/shared/menu/providers/curation.menu.spec.ts b/src/app/shared/menu/providers/curation.menu.spec.ts index d3e92fbc00..93476490b4 100644 --- a/src/app/shared/menu/providers/curation.menu.spec.ts +++ b/src/app/shared/menu/providers/curation.menu.spec.ts @@ -14,19 +14,19 @@ import { AuthorizationDataService } from '../../../core/data/feature-authorizati import { CurationMenuProvider } from './curation.menu'; import { PartialMenuSection } from '../menu-provider.model'; -const expectedSections: PartialMenuSection[] = [ - { - visible: true, - model: { - type: MenuItemType.LINK, - text: 'menu.section.curation_task', - link: 'admin/curation-tasks', - }, - icon: 'filter', - }, -]; - describe('CurationMenuProvider', () => { + const expectedSections: PartialMenuSection[] = [ + { + visible: true, + model: { + type: MenuItemType.LINK, + text: 'menu.section.curation_task', + link: 'admin/curation-tasks', + }, + icon: 'filter', + }, + ]; + let provider: CurationMenuProvider; let authorizationServiceStub = new AuthorizationDataServiceStub(); @@ -38,7 +38,7 @@ describe('CurationMenuProvider', () => { TestBed.configureTestingModule({ providers: [ CurationMenuProvider, - { provide: AuthorizationDataService, useValue: authorizationServiceStub }, + {provide: AuthorizationDataService, useValue: authorizationServiceStub}, ], }); provider = TestBed.inject(CurationMenuProvider); diff --git a/src/app/shared/menu/providers/dso-edit.menu.spec.ts b/src/app/shared/menu/providers/dso-edit.menu.spec.ts new file mode 100644 index 0000000000..15ca5c8130 --- /dev/null +++ b/src/app/shared/menu/providers/dso-edit.menu.spec.ts @@ -0,0 +1,65 @@ +import { PartialMenuSection } from '../menu-provider.model'; +import { MenuItemType } from '../menu-item-type.model'; +import { TestBed } from '@angular/core/testing'; +import { of as observableOf } from 'rxjs'; +import { AuthorizationDataService } from '../../../core/data/feature-authorization/authorization-data.service'; +import { Collection } from '../../../core/shared/collection.model'; +import { DSpaceObjectEditMenuProvider } from './dso-edit.menu'; +import { URLCombiner } from '../../../core/url-combiner/url-combiner'; +import { COLLECTION } from '../../../core/shared/collection.resource-type'; + +describe('DSpaceObjectEditMenuProvider', () => { + + const expectedSections: PartialMenuSection[] = [ + { + visible: true, + model: { + type: MenuItemType.LINK, + text: 'collection.page.edit', + link: new URLCombiner('/collections/test-uuid', 'edit', 'metadata').toString(), + }, + icon: 'pencil-alt', + } + ]; + + let provider: DSpaceObjectEditMenuProvider; + + const dso: Collection = Object.assign(new Collection(), { + type: COLLECTION.value, + uuid: 'test-uuid', + _links: {self: {href: 'self-link'}}, + }); + + + let authorizationService; + + beforeEach(() => { + + authorizationService = jasmine.createSpyObj('authorizationService', { + 'isAuthorized': observableOf(true) + }); + + TestBed.configureTestingModule({ + providers: [ + DSpaceObjectEditMenuProvider, + {provide: AuthorizationDataService, useValue: authorizationService}, + ], + }); + provider = TestBed.inject(DSpaceObjectEditMenuProvider); + }); + + it('should be created', () => { + expect(provider).toBeTruthy(); + }); + + describe('getSectionsForContext', () => { + it('should return the expected sections', (done) => { + provider.getSectionsForContext(dso).subscribe((sections) => { + expect(sections).toEqual(expectedSections); + done(); + }); + }); + }); + + +}); diff --git a/src/app/shared/menu/providers/dso-option.menu.spec.ts b/src/app/shared/menu/providers/dso-option.menu.spec.ts new file mode 100644 index 0000000000..d04c63e98d --- /dev/null +++ b/src/app/shared/menu/providers/dso-option.menu.spec.ts @@ -0,0 +1,53 @@ +import { PartialMenuSection } from '../menu-provider.model'; +import { MenuItemType } from '../menu-item-type.model'; +import { TestBed } from '@angular/core/testing'; +import { Collection } from '../../../core/shared/collection.model'; +import { COLLECTION } from '../../../core/shared/collection.resource-type'; +import { DsoOptionMenuProvider } from './dso-option.menu'; + +describe('DsoOptionMenuProvider', () => { + + const expectedSections: PartialMenuSection[] = [ + { + visible: true, + model: { + type: MenuItemType.TEXT, + text: 'collection.page.options', + }, + icon: 'ellipsis-vertical', + } + ]; + + let provider: DsoOptionMenuProvider; + + const dso: Collection = Object.assign(new Collection(), { + type: COLLECTION.value, + uuid: 'test-uuid', + _links: {self: {href: 'self-link'}}, + }); + + + beforeEach(() => { + TestBed.configureTestingModule({ + providers: [ + DsoOptionMenuProvider, + ], + }); + provider = TestBed.inject(DsoOptionMenuProvider); + }); + + it('should be created', () => { + expect(provider).toBeTruthy(); + }); + + describe('getSectionsForContext', () => { + it('should return the expected sections', (done) => { + provider.getSectionsForContext(dso).subscribe((sections) => { + expect(sections).toEqual(expectedSections); + done(); + }); + }); + }); + + +}); diff --git a/src/app/shared/menu/providers/dso-option.menu.ts b/src/app/shared/menu/providers/dso-option.menu.ts index 0fba0e19ef..45b3777a2e 100644 --- a/src/app/shared/menu/providers/dso-option.menu.ts +++ b/src/app/shared/menu/providers/dso-option.menu.ts @@ -18,7 +18,7 @@ import { DSpaceObject } from 'src/app/core/shared/dspace-object.model'; * This section will be rendered as a button on the DSO pages if sub providers have been added */ @Injectable() -export class DsoOptionMenu extends DSpaceObjectPageMenuProvider { +export class DsoOptionMenuProvider extends DSpaceObjectPageMenuProvider { alwaysRenderExpandable = true; diff --git a/src/app/shared/menu/providers/edit.menu.spec.ts b/src/app/shared/menu/providers/edit.menu.spec.ts index 570e2c4e9e..643f0efc24 100644 --- a/src/app/shared/menu/providers/edit.menu.spec.ts +++ b/src/app/shared/menu/providers/edit.menu.spec.ts @@ -15,43 +15,44 @@ import { FeatureID } from '../../../core/data/feature-authorization/feature-id'; import { EditMenuProvider } from './edit.menu'; import { PartialMenuSection } from '../menu-provider.model'; -const expectedTopSection: PartialMenuSection = { - visible: true, - model: { - type: MenuItemType.TEXT, - text: 'menu.section.edit' - }, - icon: 'pencil', -}; - -const expectedSubSections: PartialMenuSection[] = [ - { - visible: true, - model: { - type: MenuItemType.ONCLICK, - text: 'menu.section.edit_community', - function: jasmine.any(Function) as any, - }, - }, - { - visible: false, - model: { - type: MenuItemType.ONCLICK, - text: 'menu.section.edit_collection', - function: jasmine.any(Function) as any, - }, - }, - { - visible: true, - model: { - type: MenuItemType.ONCLICK, - text: 'menu.section.edit_item', - function: jasmine.any(Function) as any, - }, - }, -]; - describe('EditMenuProvider', () => { + + const expectedTopSection: PartialMenuSection = { + visible: true, + model: { + type: MenuItemType.TEXT, + text: 'menu.section.edit' + }, + icon: 'pencil', + }; + + const expectedSubSections: PartialMenuSection[] = [ + { + visible: true, + model: { + type: MenuItemType.ONCLICK, + text: 'menu.section.edit_community', + function: jasmine.any(Function) as any, + }, + }, + { + visible: false, + model: { + type: MenuItemType.ONCLICK, + text: 'menu.section.edit_collection', + function: jasmine.any(Function) as any, + }, + }, + { + visible: true, + model: { + type: MenuItemType.ONCLICK, + text: 'menu.section.edit_item', + function: jasmine.any(Function) as any, + }, + }, + ]; + let provider: EditMenuProvider; let authorizationServiceStub = new AuthorizationDataServiceStub(); @@ -67,7 +68,7 @@ describe('EditMenuProvider', () => { TestBed.configureTestingModule({ providers: [ EditMenuProvider, - { provide: AuthorizationDataService, useValue: authorizationServiceStub }, + {provide: AuthorizationDataService, useValue: authorizationServiceStub}, ], }); provider = TestBed.inject(EditMenuProvider); diff --git a/src/app/shared/menu/providers/export.menu.spec.ts b/src/app/shared/menu/providers/export.menu.spec.ts index 7e46d2811b..9ffe75627e 100644 --- a/src/app/shared/menu/providers/export.menu.spec.ts +++ b/src/app/shared/menu/providers/export.menu.spec.ts @@ -16,35 +16,35 @@ import { ScriptServiceStub } from '../../testing/script-service.stub'; import { ExportMenuProvider } from './export.menu'; import { PartialMenuSection } from '../menu-provider.model'; -const expectedTopSection: PartialMenuSection = { - visible: true, - model: { - type: MenuItemType.TEXT, - text: 'menu.section.export', - }, - icon: 'file-export', -}; - -const expectedSubSections: PartialMenuSection[] = [ - { - visible: true, - model: { - type: MenuItemType.ONCLICK, - text: 'menu.section.export_metadata', - function: jasmine.any(Function) as any, - }, - }, - { - visible: true, - model: { - type: MenuItemType.ONCLICK, - text: 'menu.section.export_batch', - function: jasmine.any(Function) as any, - }, - } -]; - describe('ExportMenuProvider', () => { + const expectedTopSection: PartialMenuSection = { + visible: true, + model: { + type: MenuItemType.TEXT, + text: 'menu.section.export', + }, + icon: 'file-export', + }; + + const expectedSubSections: PartialMenuSection[] = [ + { + visible: true, + model: { + type: MenuItemType.ONCLICK, + text: 'menu.section.export_metadata', + function: jasmine.any(Function) as any, + }, + }, + { + visible: true, + model: { + type: MenuItemType.ONCLICK, + text: 'menu.section.export_batch', + function: jasmine.any(Function) as any, + }, + } + ]; + let provider: ExportMenuProvider; let authorizationServiceStub = new AuthorizationDataServiceStub(); @@ -56,8 +56,8 @@ describe('ExportMenuProvider', () => { TestBed.configureTestingModule({ providers: [ ExportMenuProvider, - { provide: AuthorizationDataService, useValue: authorizationServiceStub }, - { provide: ScriptDataService, useClass: ScriptServiceStub }, + {provide: AuthorizationDataService, useValue: authorizationServiceStub}, + {provide: ScriptDataService, useClass: ScriptServiceStub}, ], }); provider = TestBed.inject(ExportMenuProvider); diff --git a/src/app/shared/menu/providers/health.menu.spec.ts b/src/app/shared/menu/providers/health.menu.spec.ts index 692fd2277c..82ba2ce790 100644 --- a/src/app/shared/menu/providers/health.menu.spec.ts +++ b/src/app/shared/menu/providers/health.menu.spec.ts @@ -14,19 +14,19 @@ import { AuthorizationDataService } from '../../../core/data/feature-authorizati import { HealthMenuProvider } from './health.menu'; import { PartialMenuSection } from '../menu-provider.model'; -const expectedSections: PartialMenuSection[] = [ - { - visible: true, - model: { - type: MenuItemType.LINK, - text: 'menu.section.health', - link: '/health', - }, - icon: 'heartbeat', - }, -]; - describe('HealthMenuProvider', () => { + const expectedSections: PartialMenuSection[] = [ + { + visible: true, + model: { + type: MenuItemType.LINK, + text: 'menu.section.health', + link: '/health', + }, + icon: 'heartbeat', + }, + ]; + let provider: HealthMenuProvider; let authorizationServiceStub = new AuthorizationDataServiceStub(); @@ -38,7 +38,7 @@ describe('HealthMenuProvider', () => { TestBed.configureTestingModule({ providers: [ HealthMenuProvider, - { provide: AuthorizationDataService, useValue: authorizationServiceStub }, + {provide: AuthorizationDataService, useValue: authorizationServiceStub}, ], }); provider = TestBed.inject(HealthMenuProvider); diff --git a/src/app/shared/menu/providers/helper-providers/dso.menu.spec.ts b/src/app/shared/menu/providers/helper-providers/dso.menu.spec.ts new file mode 100644 index 0000000000..5d214da5f0 --- /dev/null +++ b/src/app/shared/menu/providers/helper-providers/dso.menu.spec.ts @@ -0,0 +1,139 @@ +import { DSpaceObjectPageMenuProvider } from './dso.menu'; +import { TestBed } from '@angular/core/testing'; +import { Item } from '../../../../core/shared/item.model'; +import { ITEM } from '../../../../core/shared/item.resource-type'; +import { createSuccessfulRemoteDataObject } from '../../../remote-data.utils'; +import { Collection } from '../../../../core/shared/collection.model'; +import { COLLECTION } from '../../../../core/shared/collection.resource-type'; + + +describe('DSpaceObjectPageMenuProvider', () => { + + let provider: DSpaceObjectPageMenuProvider; + + const item: Item = Object.assign(new Item(), { + uuid: 'test-item-uuid', + type: ITEM.value, + _links: {self: {href: 'self-link'}}, + metadata: { + 'dc.title': [{ + 'value': 'Untyped Item' + }] + } + }); + + const item2: Item = Object.assign(new Item(), { + uuid: 'test-item2-uuid', + type: ITEM.value, + _links: {self: {href: 'self-link'}}, + metadata: { + 'dc.title': [{ + 'value': 'Untyped Item 2' + }] + } + }); + + const person: Item = Object.assign(new Item(), { + uuid: 'test-uuid', + type: ITEM.value, + _links: {self: {href: 'self-link'}}, + metadata: { + 'dc.title': [{ + 'value': 'Person Entity' + }], + 'dspace.entity.type': [{ + 'value': 'Person' + }], + } + }); + + const collection: Collection = Object.assign(new Collection(), { + uuid: 'test-collection-uuid', + type: COLLECTION.value, + _links: {self: {href: 'self-link'}}, + metadata: { + 'dc.title': [{ + 'value': 'Collection' + }] + } + }); + + + beforeEach(() => { + + TestBed.configureTestingModule({ + providers: [ + DSpaceObjectPageMenuProvider, + ], + }); + provider = TestBed.inject(DSpaceObjectPageMenuProvider); + }); + + it('should be created', () => { + expect(provider).toBeTruthy(); + }); + + describe('getRouteContext', () => { + it('should get the dso from the route', (done) => { + const route = {data: {dso: createSuccessfulRemoteDataObject(item)}} as any; + + provider.getRouteContext(route, undefined).subscribe((dso) => { + expect(dso).toEqual(item); + done(); + }); + }); + + it('return undefined when no DSO is present on the current route', (done) => { + const route = { + data: {}, + parent: { + data: {}, + parent: { + data: {dso: createSuccessfulRemoteDataObject(item)}, + parent: {data: {dso: createSuccessfulRemoteDataObject(item2)}} + } + } + } as any; + + provider.getRouteContext(route, undefined).subscribe((dso) => { + expect(dso).toBeUndefined(); + done(); + }); + }); + it('should return undefined when no dso is found in the route', (done) => { + const route = {data: {}, parent: {data: {}, parent: {data: {}, parent: {data: {}}}}} as any; + + provider.getRouteContext(route, undefined).subscribe((dso) => { + expect(dso).toBeUndefined(); + done(); + }); + }); + }); + + describe('getDsoType', () => { + it('should return the object type for an untyped item', () => { + const dsoType = (provider as any).getDsoType(item); + expect(dsoType).toEqual('item'); + }); + it('should return the entity type for an entity item', () => { + const dsoType = (provider as any).getDsoType(person); + expect(dsoType).toEqual('person'); + }); + it('should return the object type for a colletion', () => { + const dsoType = (provider as any).getDsoType(collection); + expect(dsoType).toEqual('collection'); + }); + }); + + describe('isApplicable', () => { + it('should return true when a dso is provided', () => { + const isApplicable = (provider as any).isApplicable(collection); + expect(isApplicable).toBeTrue(); + }); + it('should return false when no dso is provided', () => { + const isApplicable = (provider as any).isApplicable(undefined); + expect(isApplicable).toBeFalse(); + }); + }); + +}); diff --git a/src/app/shared/menu/providers/helper-providers/expandable-menu-provider.spec.ts b/src/app/shared/menu/providers/helper-providers/expandable-menu-provider.spec.ts new file mode 100644 index 0000000000..ff9cce812f --- /dev/null +++ b/src/app/shared/menu/providers/helper-providers/expandable-menu-provider.spec.ts @@ -0,0 +1,113 @@ +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ + +import { TestBed } from '@angular/core/testing'; +import { Observable, of as observableOf } from 'rxjs'; +import { AbstractExpandableMenuProvider } from './expandable-menu-provider'; +import { MenuItemType } from '../../menu-item-type.model'; +import { PartialMenuSection } from '../../menu-provider.model'; +import { MenuID } from '../../menu-id.model'; + + +describe('AbstractExpandableMenuProvider', () => { + const topSection: PartialMenuSection = { + visible: true, + model: { + type: MenuItemType.TEXT, + text: 'top.section.test', + }, + icon: 'file-import', + }; + + const subSections: PartialMenuSection[] = [ + { + visible: true, + model: { + type: MenuItemType.TEXT, + text: 'sub.section.test.1', + }, + }, + { + visible: true, + model: { + type: MenuItemType.TEXT, + text: 'sub.section.test.2', + }, + }, + ]; + + class TestClass extends AbstractExpandableMenuProvider { + getTopSection(): Observable { + return observableOf(topSection); + } + + getSubSections(): Observable { + return observableOf(subSections); + } + + } + + const expectedSections: PartialMenuSection[] = [ + { + visible: true, + model: { + type: MenuItemType.TEXT, + text: 'sub.section.test.1', + }, + id: `${MenuID.ADMIN}_1_0`, + parentID: `${MenuID.ADMIN}_1`, + alwaysRenderExpandable: false, + }, + { + visible: true, + model: { + type: MenuItemType.TEXT, + text: 'sub.section.test.2', + }, + id: `${MenuID.ADMIN}_1_1`, + parentID: `${MenuID.ADMIN}_1`, + alwaysRenderExpandable: false, + }, + { + visible: true, + model: { + type: MenuItemType.TEXT, + text: 'top.section.test', + }, + icon: 'file-import', + id: `${MenuID.ADMIN}_1`, + alwaysRenderExpandable: true, + } + ]; + + let provider: AbstractExpandableMenuProvider; + + beforeEach(() => { + + TestBed.configureTestingModule({ + providers: [ + TestClass, + ], + }); + provider = TestBed.inject(TestClass); + provider.menuProviderId = `${MenuID.ADMIN}_1`; + }); + + it('should be created', () => { + expect(provider).toBeTruthy(); + }); + + it('getSections should return a combination of top and sub sections', (done) => { + + provider.getSections().subscribe((section) => { + expect(section).toEqual(expectedSections); + done(); + }); + }); + +}); diff --git a/src/app/shared/menu/providers/helper-providers/route-context.menu.spec.ts b/src/app/shared/menu/providers/helper-providers/route-context.menu.spec.ts new file mode 100644 index 0000000000..1120681c05 --- /dev/null +++ b/src/app/shared/menu/providers/helper-providers/route-context.menu.spec.ts @@ -0,0 +1,96 @@ +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ + +import { TestBed } from '@angular/core/testing'; +import { Observable, of as observableOf } from 'rxjs'; +import { MenuItemType } from '../../menu-item-type.model'; +import { PartialMenuSection } from '../../menu-provider.model'; +import { MenuID } from '../../menu-id.model'; +import { AbstractRouteContextMenuProvider } from './route-context.menu'; +import { CacheableObject } from '../../../../core/cache/cacheable-object.model'; +import { ActivatedRouteSnapshot, RouterStateSnapshot } from '@angular/router'; + + +describe('AbstractExpandableMenuProvider', () => { + + class TestClass extends AbstractRouteContextMenuProvider { + getRouteContext(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable { + return observableOf(object); + } + + getSectionsForContext(routeContext: CacheableObject): Observable { + return observableOf(expectedSections); + } + } + + const object = Object.assign(new CacheableObject()); + + + const expectedSections: PartialMenuSection[] = [ + { + visible: true, + model: { + type: MenuItemType.TEXT, + text: 'sub.section.test.1', + }, + id: `${MenuID.ADMIN}_1_0`, + parentID: `${MenuID.ADMIN}_1`, + alwaysRenderExpandable: false, + }, + { + visible: true, + model: { + type: MenuItemType.TEXT, + text: 'sub.section.test.2', + }, + id: `${MenuID.ADMIN}_1_1`, + parentID: `${MenuID.ADMIN}_1`, + alwaysRenderExpandable: false, + }, + { + visible: true, + model: { + type: MenuItemType.TEXT, + text: 'top.section.test', + }, + icon: 'file-import', + id: `${MenuID.ADMIN}_1`, + alwaysRenderExpandable: true, + } + ]; + + let provider: AbstractRouteContextMenuProvider; + + beforeEach(() => { + + TestBed.configureTestingModule({ + providers: [ + TestClass, + ], + }); + provider = TestBed.inject(TestClass); + provider.menuProviderId = `${MenuID.ADMIN}_1`; + }); + + it('should be created', () => { + expect(provider).toBeTruthy(); + }); + + it('getSections should return the sections based on the retrieved route context and sections for that context', (done) => { + spyOn(provider, 'getRouteContext').and.callThrough(); + spyOn(provider, 'getSectionsForContext').and.callThrough(); + + provider.getSections(undefined, undefined).subscribe((sections) => { + expect(sections).toEqual(expectedSections); + expect(provider.getRouteContext).toHaveBeenCalled(); + expect(provider.getSectionsForContext).toHaveBeenCalledWith(object); + done(); + }); + }); + +}); 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 bd03bcc0f3..56bd091f2b 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 @@ -24,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.spec.ts b/src/app/shared/menu/providers/import.menu.spec.ts index 183dead399..f882a17ad6 100644 --- a/src/app/shared/menu/providers/import.menu.spec.ts +++ b/src/app/shared/menu/providers/import.menu.spec.ts @@ -16,35 +16,35 @@ import { ScriptDataService } from '../../../core/data/processes/script-data.serv import { ScriptServiceStub } from '../../testing/script-service.stub'; import { PartialMenuSection } from '../menu-provider.model'; -const expectedTopSection: PartialMenuSection = { - visible: true, - model: { - type: MenuItemType.TEXT, - text: 'menu.section.import', - }, - icon: 'file-import', -}; - -const expectedSubSections: PartialMenuSection[] = [ - { - visible: true, - model: { - type: MenuItemType.LINK, - text: 'menu.section.import_metadata', - link: '/admin/metadata-import', - }, - }, - { - visible: true, - model: { - type: MenuItemType.LINK, - text: 'menu.section.import_batch', - link: '/admin/batch-import', - }, - }, -]; - describe('ImportMenuProvider', () => { + const expectedTopSection: PartialMenuSection = { + visible: true, + model: { + type: MenuItemType.TEXT, + text: 'menu.section.import', + }, + icon: 'file-import', + }; + + const expectedSubSections: PartialMenuSection[] = [ + { + visible: true, + model: { + type: MenuItemType.LINK, + text: 'menu.section.import_metadata', + link: '/admin/metadata-import', + }, + }, + { + visible: true, + model: { + type: MenuItemType.LINK, + text: 'menu.section.import_batch', + link: '/admin/batch-import', + }, + }, + ]; + let provider: ImportMenuProvider; let authorizationServiceStub = new AuthorizationDataServiceStub(); @@ -56,8 +56,8 @@ describe('ImportMenuProvider', () => { TestBed.configureTestingModule({ providers: [ ImportMenuProvider, - { provide: AuthorizationDataService, useValue: authorizationServiceStub }, - { provide: ScriptDataService, useClass: ScriptServiceStub }, + {provide: AuthorizationDataService, useValue: authorizationServiceStub}, + {provide: ScriptDataService, useClass: ScriptServiceStub}, ], }); provider = TestBed.inject(ImportMenuProvider); diff --git a/src/app/shared/menu/providers/item-claim.menu.spec.ts b/src/app/shared/menu/providers/item-claim.menu.spec.ts new file mode 100644 index 0000000000..0889dc6713 --- /dev/null +++ b/src/app/shared/menu/providers/item-claim.menu.spec.ts @@ -0,0 +1,134 @@ +import { PartialMenuSection } from '../menu-provider.model'; +import { MenuItemType } from '../menu-item-type.model'; +import { TestBed } from '@angular/core/testing'; +import { of as observableOf } from 'rxjs'; +import { AuthorizationDataService } from '../../../core/data/feature-authorization/authorization-data.service'; +import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; +import { ClaimMenuProvider } from './item-claim.menu'; +import { Item } from '../../../core/shared/item.model'; +import { TranslateModule } from '@ngx-translate/core'; +import { NotificationsServiceStub } from '../../testing/notifications-service.stub'; +import { MenuService } from '../menu.service'; +import { NotificationsService } from '../../notifications/notifications.service'; +import { ResearcherProfileDataService } from '../../../core/profile/researcher-profile-data.service'; +import { ITEM } from '../../../core/shared/item.resource-type'; + +describe('ClaimMenuProvider', () => { + + const expectedSections: PartialMenuSection[] = [ + { + visible: true, + model: { + type: MenuItemType.ONCLICK, + text: 'item.page.claim.button', + function: jasmine.any(Function) as any, + }, + icon: 'hand-paper', + } + ]; + + let provider: ClaimMenuProvider; + + const item: Item = Object.assign(new Item(), { + type: ITEM.value, + _links: {self: {href: 'self-link'}}, + metadata: { + 'dc.title': [{ + 'value': 'Untyped Item' + }] + } + + }); + const person: Item = Object.assign(new Item(), { + type: ITEM.value, + _links: {self: {href: 'self-link'}}, + metadata: { + 'dc.title': [{ + 'value': 'Person Entity' + }], + 'dspace.entity.type': [{ + 'value': 'Person' + }], + } + }); + + + let authorizationService; + let menuService; + let notificationsService; + let researcherProfileService; + let modalService; + + + beforeEach(() => { + + authorizationService = jasmine.createSpyObj('authorizationService', { + 'isAuthorized': observableOf(true), + 'invalidateAuthorizationsRequestCache': {} + }); + + menuService = jasmine.createSpyObj('menuService', ['hideMenuSection']); + + notificationsService = new NotificationsServiceStub(); + + researcherProfileService = jasmine.createSpyObj('authorizationService', { + 'createFromExternalSourceAndReturnRelatedItemId': observableOf('profile-id') + }); + + modalService = jasmine.createSpyObj('modalService', ['open']); + + + TestBed.configureTestingModule({ + imports: [TranslateModule.forRoot()], + providers: [ + ClaimMenuProvider, + {provide: AuthorizationDataService, useValue: authorizationService}, + {provide: MenuService, useValue: menuService}, + {provide: NotificationsService, useValue: notificationsService}, + {provide: ResearcherProfileDataService, useValue: researcherProfileService}, + {provide: NgbModal, useValue: modalService}, + ], + }); + provider = TestBed.inject(ClaimMenuProvider); + }); + + it('should be created', () => { + expect(provider).toBeTruthy(); + }); + + describe('getSectionsForContext', () => { + it('should return the expected sections', (done) => { + provider.getSectionsForContext(person).subscribe((sections) => { + expect(sections).toEqual(expectedSections); + done(); + }); + }); + }); + + describe('isApplicable', () => { + it('should return true whe the provided dspace object is a person entity', () => { + const result = (provider as any).isApplicable(person); + expect(result).toBeTrue(); + }); + it('should return true whe the provided dspace object is not a person entity', () => { + const result = (provider as any).isApplicable(item); + expect(result).toBeFalse(); + }); + }); + + describe('claimResearcher', () => { + it('should show a success notification and hide the menu when an id is returned by the researcher profile service', () => { + (provider as any).claimResearcher(person); + expect(notificationsService.success).toHaveBeenCalled(); + expect(authorizationService.invalidateAuthorizationsRequestCache).toHaveBeenCalled(); + expect(menuService.hideMenuSection).toHaveBeenCalled(); + }); + it('should show an error notification when no id is returned by the researcher profile service', () => { + (researcherProfileService.createFromExternalSourceAndReturnRelatedItemId as jasmine.Spy).and.returnValue(observableOf(null)); + (provider as any).claimResearcher(person); + expect(notificationsService.error).toHaveBeenCalled(); + expect(authorizationService.invalidateAuthorizationsRequestCache).not.toHaveBeenCalled(); + expect(menuService.hideMenuSection).not.toHaveBeenCalled(); + }); + }); +}); diff --git a/src/app/shared/menu/providers/item-claim.menu.ts b/src/app/shared/menu/providers/item-claim.menu.ts index 92b27fc02e..06f26a773d 100644 --- a/src/app/shared/menu/providers/item-claim.menu.ts +++ b/src/app/shared/menu/providers/item-claim.menu.ts @@ -40,14 +40,6 @@ export class ClaimMenuProvider extends DSpaceObjectPageMenuProvider { super(); } - - protected isApplicable(item: DSpaceObject): boolean { - if (item instanceof Item) { - return this.getDsoType(item) === 'person'; - } - return false; - } - public getSectionsForContext(item: Item): Observable { return combineLatest([ this.authorizationService.isAuthorized(FeatureID.CanClaimItem, item.self), @@ -70,6 +62,13 @@ export class ClaimMenuProvider extends DSpaceObjectPageMenuProvider { ); } + protected isApplicable(item: DSpaceObject): boolean { + if (item instanceof Item) { + return this.getDsoType(item) === 'person'; + } + return false; + } + /** * Claim a researcher by creating a profile * Shows notifications and/or hides the menu section on success/error @@ -82,7 +81,7 @@ export class ClaimMenuProvider extends DSpaceObjectPageMenuProvider { this.translate.get('researcherprofile.success.claim.body'), ); this.authorizationService.invalidateAuthorizationsRequestCache(); - this.menuService.hideMenuSection(MenuID.DSO_EDIT, 'claim-dso-' + item.uuid); + this.menuService.hideMenuSection(MenuID.DSO_EDIT, this.menuProviderId); } else { this.notificationsService.error( this.translate.get('researcherprofile.error.claim.title'), diff --git a/src/app/shared/menu/providers/item-orcid.menu.spec.ts b/src/app/shared/menu/providers/item-orcid.menu.spec.ts new file mode 100644 index 0000000000..127e583ce2 --- /dev/null +++ b/src/app/shared/menu/providers/item-orcid.menu.spec.ts @@ -0,0 +1,94 @@ +import { PartialMenuSection } from '../menu-provider.model'; +import { MenuItemType } from '../menu-item-type.model'; +import { TestBed } from '@angular/core/testing'; +import { of as observableOf } from 'rxjs'; +import { AuthorizationDataService } from '../../../core/data/feature-authorization/authorization-data.service'; +import { Item } from '../../../core/shared/item.model'; +import { TranslateModule } from '@ngx-translate/core'; +import { ITEM } from '../../../core/shared/item.resource-type'; +import { URLCombiner } from '../../../core/url-combiner/url-combiner'; +import { OrcidMenuProvider } from './item-orcid.menu'; + +describe('OrcidMenuProvider', () => { + + const expectedSections: PartialMenuSection[] = [ + { + visible: true, + model: { + type: MenuItemType.LINK, + text: 'item.page.orcid.tooltip', + link: new URLCombiner('/entities/person/test-uuid', 'orcid').toString(), + }, + icon: 'orcid fab fa-lg', + } + ]; + + let provider: OrcidMenuProvider; + + const item: Item = Object.assign(new Item(), { + type: ITEM.value, + _links: {self: {href: 'self-link'}}, + metadata: { + 'dc.title': [{ + 'value': 'Untyped Item' + }] + } + }); + + const person: Item = Object.assign(new Item(), { + uuid: 'test-uuid', + type: ITEM.value, + _links: {self: {href: 'self-link'}}, + metadata: { + 'dc.title': [{ + 'value': 'Person Entity' + }], + 'dspace.entity.type': [{ + 'value': 'Person' + }], + } + }); + + + let authorizationService; + + beforeEach(() => { + authorizationService = jasmine.createSpyObj('authorizationService', { + 'isAuthorized': observableOf(true), + }); + + TestBed.configureTestingModule({ + imports: [TranslateModule.forRoot()], + providers: [ + OrcidMenuProvider, + {provide: AuthorizationDataService, useValue: authorizationService}, + ], + }); + provider = TestBed.inject(OrcidMenuProvider); + }); + + it('should be created', () => { + expect(provider).toBeTruthy(); + }); + + describe('getSectionsForContext', () => { + it('should return the expected sections', (done) => { + provider.getSectionsForContext(person).subscribe((sections) => { + expect(sections).toEqual(expectedSections); + done(); + }); + }); + }); + + describe('isApplicable', () => { + it('should return true whe the provided dspace object is a person entity', () => { + const result = (provider as any).isApplicable(person); + expect(result).toBeTrue(); + }); + it('should return true whe the provided dspace object is not a person entity', () => { + const result = (provider as any).isApplicable(item); + expect(result).toBeFalse(); + }); + }); + +}); diff --git a/src/app/shared/menu/providers/item-orcid.menu.ts b/src/app/shared/menu/providers/item-orcid.menu.ts index 208a3b26c6..3dc916ee6d 100644 --- a/src/app/shared/menu/providers/item-orcid.menu.ts +++ b/src/app/shared/menu/providers/item-orcid.menu.ts @@ -29,13 +29,6 @@ export class OrcidMenuProvider extends DSpaceObjectPageMenuProvider { super(); } - protected isApplicable(item: Item): boolean { - if (item instanceof Item) { - return this.getDsoType(item) === 'person'; - } - return false; - } - public getSectionsForContext(item: Item): Observable { return combineLatest([ this.authorizationService.isAuthorized(FeatureID.CanSynchronizeWithORCID, item.self), @@ -55,4 +48,11 @@ export class OrcidMenuProvider extends DSpaceObjectPageMenuProvider { }), ); } + + protected isApplicable(item: Item): boolean { + if (item instanceof Item) { + return this.getDsoType(item) === 'person'; + } + return false; + } } diff --git a/src/app/shared/menu/providers/item-versioning.menu.spec.ts b/src/app/shared/menu/providers/item-versioning.menu.spec.ts new file mode 100644 index 0000000000..e5a80e78b7 --- /dev/null +++ b/src/app/shared/menu/providers/item-versioning.menu.spec.ts @@ -0,0 +1,98 @@ +import { PartialMenuSection } from '../menu-provider.model'; +import { MenuItemType } from '../menu-item-type.model'; +import { TestBed } from '@angular/core/testing'; +import { of as observableOf } from 'rxjs'; +import { AuthorizationDataService } from '../../../core/data/feature-authorization/authorization-data.service'; +import { Item } from '../../../core/shared/item.model'; +import { TranslateModule } from '@ngx-translate/core'; +import { ITEM } from '../../../core/shared/item.resource-type'; +import { VersioningMenuProvider } from './item-versioning.menu'; +import { DsoVersioningModalService } from '../../dso-page/dso-versioning-modal-service/dso-versioning-modal.service'; + +describe('VersioningMenuProvider', () => { + + const expectedSectionsWhenVersionNotPresent: PartialMenuSection[] = [ + { + visible: true, + model: { + type: MenuItemType.ONCLICK, + text: 'item.page.version.create', + disabled: false, + function: jasmine.any(Function) as any, + }, + icon: 'code-branch', + } + ]; + const expectedSectionsWhenVersionPresent: PartialMenuSection[] = [ + { + visible: true, + model: { + type: MenuItemType.ONCLICK, + text: 'item.page.version.hasDraft', + disabled: true, + function: jasmine.any(Function) as any, + }, + icon: 'code-branch', + } + ]; + + let provider: VersioningMenuProvider; + + const item: Item = Object.assign(new Item(), { + type: ITEM.value, + _links: {self: {href: 'self-link'}}, + metadata: { + 'dc.title': [{ + 'value': 'Untyped Item' + }] + } + }); + + + let authorizationService; + let dsoVersioningModalService; + + beforeEach(() => { + authorizationService = jasmine.createSpyObj('authorizationService', { + isAuthorized: observableOf(true), + }); + + dsoVersioningModalService = jasmine.createSpyObj('dsoVersioningModalService', { + isNewVersionButtonDisabled: observableOf(false), + getVersioningTooltipMessage: observableOf('item.page.version.create'), + }); + + TestBed.configureTestingModule({ + imports: [TranslateModule.forRoot()], + providers: [ + VersioningMenuProvider, + {provide: AuthorizationDataService, useValue: authorizationService}, + {provide: DsoVersioningModalService, useValue: dsoVersioningModalService}, + ], + }); + provider = TestBed.inject(VersioningMenuProvider); + }); + + it('should be created', () => { + expect(provider).toBeTruthy(); + }); + + describe('getSectionsForContext', () => { + it('should return the section to create a new version when no version draft is present yet', (done) => { + provider.getSectionsForContext(item).subscribe((sections) => { + expect(sections).toEqual(expectedSectionsWhenVersionNotPresent); + done(); + }); + }); + it('should return the section to that a version is present when a version draft is present', (done) => { + (dsoVersioningModalService.isNewVersionButtonDisabled as jasmine.Spy).and.returnValue(observableOf(true)); + (dsoVersioningModalService.getVersioningTooltipMessage as jasmine.Spy).and.returnValue(observableOf('item.page.version.hasDraft')); + + provider.getSectionsForContext(item).subscribe((sections) => { + expect(sections).toEqual(expectedSectionsWhenVersionPresent); + done(); + }); + }); + }); + +}); diff --git a/src/app/shared/menu/providers/new.menu.spec.ts b/src/app/shared/menu/providers/new.menu.spec.ts index c03f788275..01fa942548 100644 --- a/src/app/shared/menu/providers/new.menu.spec.ts +++ b/src/app/shared/menu/providers/new.menu.spec.ts @@ -15,51 +15,51 @@ import { AuthorizationDataService } from '../../../core/data/feature-authorizati import { FeatureID } from '../../../core/data/feature-authorization/feature-id'; import { PartialMenuSection } from '../menu-provider.model'; -const expectedTopSection: PartialMenuSection = { - visible: true, - model: { +describe('NewMenuProvider', () => { + const expectedTopSection: PartialMenuSection = { + visible: true, + model: { type: MenuItemType.TEXT, text: 'menu.section.new' }, icon: 'plus', }; -const expectedSubSections: PartialMenuSection[] = [ - { - visible: true, - model: { - type: MenuItemType.ONCLICK, - text: 'menu.section.new_community', - function: jasmine.any(Function) as any, + const expectedSubSections: PartialMenuSection[] = [ + { + visible: true, + model: { + type: MenuItemType.ONCLICK, + text: 'menu.section.new_community', + function: jasmine.any(Function) as any, + }, }, - }, - { - visible: false, - model: { - type: MenuItemType.ONCLICK, - text: 'menu.section.new_collection', - function: jasmine.any(Function) as any, + { + visible: false, + model: { + type: MenuItemType.ONCLICK, + text: 'menu.section.new_collection', + function: jasmine.any(Function) as any, + }, }, - }, - { - visible: true, - model: { - type: MenuItemType.ONCLICK, - text: 'menu.section.new_item', - function: jasmine.any(Function) as any, + { + visible: true, + model: { + type: MenuItemType.ONCLICK, + text: 'menu.section.new_item', + function: jasmine.any(Function) as any, + }, }, - }, - { - visible: true, - model: { - type: MenuItemType.LINK, - text: 'menu.section.new_process', - link: '/processes/new' + { + visible: true, + model: { + type: MenuItemType.LINK, + text: 'menu.section.new_process', + link: '/processes/new' + }, }, - }, -]; + ]; -describe('NewMenuProvider', () => { let provider: NewMenuProvider; let authorizationServiceStub = new AuthorizationDataServiceStub(); @@ -75,7 +75,7 @@ describe('NewMenuProvider', () => { TestBed.configureTestingModule({ providers: [ NewMenuProvider, - { provide: AuthorizationDataService, useValue: authorizationServiceStub }, + {provide: AuthorizationDataService, useValue: authorizationServiceStub}, ], }); provider = TestBed.inject(NewMenuProvider); diff --git a/src/app/shared/menu/providers/processes.menu.spec.ts b/src/app/shared/menu/providers/processes.menu.spec.ts index eb72a02765..b0d353634c 100644 --- a/src/app/shared/menu/providers/processes.menu.spec.ts +++ b/src/app/shared/menu/providers/processes.menu.spec.ts @@ -14,19 +14,20 @@ import { AuthorizationDataService } from '../../../core/data/feature-authorizati import { ProcessesMenuProvider } from './processes.menu'; import { PartialMenuSection } from '../menu-provider.model'; -const expectedSections: PartialMenuSection[] = [ - { - visible: true, - model: { - type: MenuItemType.LINK, - text: 'menu.section.processes', - link: '/processes', - }, - icon: 'terminal', - }, -]; - describe('ProcessesMenuProvider', () => { + + const expectedSections: PartialMenuSection[] = [ + { + visible: true, + model: { + type: MenuItemType.LINK, + text: 'menu.section.processes', + link: '/processes', + }, + icon: 'terminal', + }, + ]; + let provider: ProcessesMenuProvider; let authorizationServiceStub = new AuthorizationDataServiceStub(); @@ -38,7 +39,7 @@ describe('ProcessesMenuProvider', () => { TestBed.configureTestingModule({ providers: [ ProcessesMenuProvider, - { provide: AuthorizationDataService, useValue: authorizationServiceStub }, + {provide: AuthorizationDataService, useValue: authorizationServiceStub}, ], }); provider = TestBed.inject(ProcessesMenuProvider); diff --git a/src/app/shared/menu/providers/registries.menu.spec.ts b/src/app/shared/menu/providers/registries.menu.spec.ts index ae8c419163..2a182d606b 100644 --- a/src/app/shared/menu/providers/registries.menu.spec.ts +++ b/src/app/shared/menu/providers/registries.menu.spec.ts @@ -16,35 +16,36 @@ import { ScriptServiceStub } from '../../testing/script-service.stub'; import { RegistriesMenuProvider } from './registries.menu'; import { PartialMenuSection } from '../menu-provider.model'; -const expectedTopSection: PartialMenuSection = { - visible: true, - model: { - type: MenuItemType.TEXT, - text: 'menu.section.registries', - }, - icon: 'list', -}; - -const expectedSubSections: PartialMenuSection[] = [ - { - visible: true, - model: { - type: MenuItemType.LINK, - text: 'menu.section.registries_metadata', - link: 'admin/registries/metadata', - }, - }, - { - visible: true, - model: { - type: MenuItemType.LINK, - text: 'menu.section.registries_format', - link: 'admin/registries/bitstream-formats', - }, - }, -]; - describe('RegistriesMenuProvider', () => { + + const expectedTopSection: PartialMenuSection = { + visible: true, + model: { + type: MenuItemType.TEXT, + text: 'menu.section.registries', + }, + icon: 'list', + }; + + const expectedSubSections: PartialMenuSection[] = [ + { + visible: true, + model: { + type: MenuItemType.LINK, + text: 'menu.section.registries_metadata', + link: 'admin/registries/metadata', + }, + }, + { + visible: true, + model: { + type: MenuItemType.LINK, + text: 'menu.section.registries_format', + link: 'admin/registries/bitstream-formats', + }, + }, + ]; + let provider: RegistriesMenuProvider; let authorizationServiceStub = new AuthorizationDataServiceStub(); @@ -56,8 +57,8 @@ describe('RegistriesMenuProvider', () => { TestBed.configureTestingModule({ providers: [ RegistriesMenuProvider, - { provide: AuthorizationDataService, useValue: authorizationServiceStub }, - { provide: ScriptDataService, useClass: ScriptServiceStub }, + {provide: AuthorizationDataService, useValue: authorizationServiceStub}, + {provide: ScriptDataService, useClass: ScriptServiceStub}, ], }); provider = TestBed.inject(RegistriesMenuProvider); diff --git a/src/app/shared/menu/providers/statistics.menu.spec.ts b/src/app/shared/menu/providers/statistics.menu.spec.ts new file mode 100644 index 0000000000..0e5dbf83c7 --- /dev/null +++ b/src/app/shared/menu/providers/statistics.menu.spec.ts @@ -0,0 +1,127 @@ +import { PartialMenuSection } from '../menu-provider.model'; +import { MenuItemType } from '../menu-item-type.model'; +import { TestBed } from '@angular/core/testing'; +import { Item } from '../../../core/shared/item.model'; +import { TranslateModule } from '@ngx-translate/core'; +import { ITEM } from '../../../core/shared/item.resource-type'; +import { StatisticsMenuProvider } from './statistics.menu'; +import { createSuccessfulRemoteDataObject } from '../../remote-data.utils'; + +describe('StatisticsMenuProvider', () => { + + const expectedSectionsNoDSO: PartialMenuSection[] = [ + { + visible: true, + model: { + type: MenuItemType.LINK, + text: 'menu.section.statistics', + link: `statistics`, + }, + icon: 'chart-line', + } + ]; + + const expectedSectionsForItem: PartialMenuSection[] = [ + { + visible: true, + model: { + type: MenuItemType.LINK, + text: 'menu.section.statistics', + link: `statistics/items/test-item-uuid`, + }, + icon: 'chart-line', + } + ]; + + let provider: StatisticsMenuProvider; + + const item: Item = Object.assign(new Item(), { + uuid: 'test-item-uuid', + type: ITEM.value, + _links: {self: {href: 'self-link'}}, + metadata: { + 'dc.title': [{ + 'value': 'Untyped Item' + }] + } + }); + + const item2: Item = Object.assign(new Item(), { + uuid: 'test-item2-uuid', + type: ITEM.value, + _links: {self: {href: 'self-link'}}, + metadata: { + 'dc.title': [{ + 'value': 'Untyped Item 2' + }] + } + }); + + beforeEach(() => { + + TestBed.configureTestingModule({ + imports: [TranslateModule.forRoot()], + providers: [ + StatisticsMenuProvider, + ], + }); + provider = TestBed.inject(StatisticsMenuProvider); + }); + + it('should be created', () => { + expect(provider).toBeTruthy(); + }); + + describe('getSectionsForContext', () => { + it('should return the general statistics link when no DSO is provided', (done) => { + provider.getSectionsForContext(undefined).subscribe((sections) => { + expect(sections).toEqual(expectedSectionsNoDSO); + done(); + }); + }); + it('should return a statistics link to the DSO when a DSO is provided', (done) => { + provider.getSectionsForContext(item).subscribe((sections) => { + expect(sections).toEqual(expectedSectionsForItem); + done(); + }); + }); + }); + + describe('getRouteContext', () => { + it('should get the dso from the route', (done) => { + const route = {data: {dso: createSuccessfulRemoteDataObject(item)}} as any; + + provider.getRouteContext(route, undefined).subscribe((dso) => { + expect(dso).toEqual(item); + done(); + }); + }); + it('should get the dso from first parent route with a dso when the route itself has none', (done) => { + const route = { + data: {}, + parent: { + data: {}, + parent: { + data: {dso: createSuccessfulRemoteDataObject(item)}, + parent: {data: {dso: createSuccessfulRemoteDataObject(item2)}} + } + } + } as any; + + provider.getRouteContext(route, undefined).subscribe((dso) => { + expect(dso).toEqual(item); + expect(dso).not.toEqual(item2); + done(); + }); + }); + it('should return undefined when no dso is found in the route', (done) => { + const route = {data: {}, parent: {data: {}, parent: {data: {}, parent: {data: {}}}}} as any; + + provider.getRouteContext(route, undefined).subscribe((dso) => { + expect(dso).toBeUndefined(); + done(); + }); + }); + }); + +}); diff --git a/src/app/shared/menu/providers/statistics.menu.ts b/src/app/shared/menu/providers/statistics.menu.ts index 4690cdf1aa..356f2f53d2 100644 --- a/src/app/shared/menu/providers/statistics.menu.ts +++ b/src/app/shared/menu/providers/statistics.menu.ts @@ -49,7 +49,7 @@ export class StatisticsMenuProvider extends AbstractRouteContextMenuProvider { + + const expectedSections: PartialMenuSection[] = [ + { + visible: true, + model: { + type: MenuItemType.LINK, + text: 'menu.section.system-wide-alert', + link: '/admin/system-wide-alert', + }, + icon: 'exclamation-circle', + }, + ]; + let provider: SystemWideAlertMenuProvider; let authorizationServiceStub = new AuthorizationDataServiceStub(); @@ -38,7 +39,7 @@ describe('SystemWideAlertMenuProvider', () => { TestBed.configureTestingModule({ providers: [ SystemWideAlertMenuProvider, - { provide: AuthorizationDataService, useValue: authorizationServiceStub }, + {provide: AuthorizationDataService, useValue: authorizationServiceStub}, ], }); provider = TestBed.inject(SystemWideAlertMenuProvider); diff --git a/src/app/shared/menu/providers/workflow.menu.spec.ts b/src/app/shared/menu/providers/workflow.menu.spec.ts index 14b14db115..5c92bae656 100644 --- a/src/app/shared/menu/providers/workflow.menu.spec.ts +++ b/src/app/shared/menu/providers/workflow.menu.spec.ts @@ -14,19 +14,20 @@ import { AuthorizationDataService } from '../../../core/data/feature-authorizati import { WorkflowMenuProvider } from './workflow.menu'; import { PartialMenuSection } from '../menu-provider.model'; -const expectedSections: PartialMenuSection[] = [ - { - visible: true, - model: { - type: MenuItemType.LINK, - text: 'menu.section.workflow', - link: '/admin/workflow', - }, - icon: 'user-check', - }, -]; - describe('WorkflowMenuProvider', () => { + + const expectedSections: PartialMenuSection[] = [ + { + visible: true, + model: { + type: MenuItemType.LINK, + text: 'menu.section.workflow', + link: '/admin/workflow', + }, + icon: 'user-check', + }, + ]; + let provider: WorkflowMenuProvider; let authorizationServiceStub = new AuthorizationDataServiceStub(); @@ -38,7 +39,7 @@ describe('WorkflowMenuProvider', () => { TestBed.configureTestingModule({ providers: [ WorkflowMenuProvider, - { provide: AuthorizationDataService, useValue: authorizationServiceStub }, + {provide: AuthorizationDataService, useValue: authorizationServiceStub}, ], }); provider = TestBed.inject(WorkflowMenuProvider); From 4f1013a20d1be64c2517d15bbf6ef94b622ce72a Mon Sep 17 00:00:00 2001 From: Yana De Pauw Date: Mon, 20 Jan 2025 13:27:36 +0100 Subject: [PATCH 13/32] Fix small issues --- src/app/app.menus.ts | 1 - src/app/shared/menu/menu-provider.model.ts | 7 +++++-- src/app/shared/menu/menu-provider.service.ts | 15 ++++++++------- src/app/shared/menu/menu.structure.ts | 2 +- .../helper-providers/expandable-menu-provider.ts | 8 ++++++++ src/app/shared/menu/providers/item-claim.menu.ts | 2 +- 6 files changed, 23 insertions(+), 12 deletions(-) diff --git a/src/app/app.menus.ts b/src/app/app.menus.ts index 8240a9cb1b..b4989b4bb6 100644 --- a/src/app/app.menus.ts +++ b/src/app/app.menus.ts @@ -73,7 +73,6 @@ export const MENUS = buildMenuStructure({ ClaimMenuProvider.onRoute( MenuRoute.SIMPLE_ITEM_PAGE, MenuRoute.FULL_ITEM_PAGE, - MenuRoute.SIMPLE_COLLECTION_PAGE, ), ]), ], diff --git a/src/app/shared/menu/menu-provider.model.ts b/src/app/shared/menu/menu-provider.model.ts index 8a517934b9..026ef034bd 100644 --- a/src/app/shared/menu/menu-provider.model.ts +++ b/src/app/shared/menu/menu-provider.model.ts @@ -22,7 +22,6 @@ export interface PartialMenuSection { visible: boolean; model: MenuItemModels; parentID?: string; - index?: number; active?: boolean; shouldPersistOnRouteChange?: boolean; icon?: string; @@ -65,7 +64,7 @@ export abstract class AbstractMenuProvider implements MenuProvider { /** * ID of the menu this provider is part of - * If not set up, this will be set based on the provider class name + * This will be set to the menu ID of the menu in which it is present in the app.menus.ts file */ menuID?: MenuID; @@ -144,6 +143,10 @@ export abstract class AbstractMenuProvider implements MenuProvider { */ abstract getSections(route?: ActivatedRouteSnapshot, state?: RouterStateSnapshot): Observable; + protected getAutomatedSectionId(indexOfSectionInProvider: number): string { + return `${this.menuProviderId}_${indexOfSectionInProvider};` + } + } diff --git a/src/app/shared/menu/menu-provider.service.ts b/src/app/shared/menu/menu-provider.service.ts index c0f34be481..2ef19010c1 100644 --- a/src/app/shared/menu/menu-provider.service.ts +++ b/src/app/shared/menu/menu-provider.service.ts @@ -92,8 +92,8 @@ export class MenuProviderService { provider: AbstractMenuProvider, sections: PartialMenuSection[] }, sectionIndex) => { - providerWithSection.sections.forEach((section) => { - this.addSection(providerWithSection.provider, section); + providerWithSection.sections.forEach((section, index) => { + this.addSection(providerWithSection.provider, section, index); }); return this.waitForMenu$(providerWithSection.provider.menuID); }); @@ -156,8 +156,8 @@ export class MenuProviderService { provider: AbstractMenuProvider, sections: PartialMenuSection[] }) => { - providerWithSection.sections.forEach((section) => { - this.addSection(providerWithSection.provider, section); + providerWithSection.sections.forEach((section, index) => { + this.addSection(providerWithSection.provider, section, index); }); return this.waitForMenu$(providerWithSection.provider.menuID); }); @@ -172,12 +172,13 @@ export class MenuProviderService { * @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) { + private addSection(provider: AbstractMenuProvider, section: PartialMenuSection, index: number) { this.menuService.addSection(provider.menuID, { ...section, - id: section.id ?? `${provider.menuProviderId}`, + id: section.id ?? `${provider.menuProviderId}_${index}`, parentID: section.parentID ?? provider.parentID, - index: section.index ?? provider.index, + index: provider.index, + active: section.active ?? true, shouldPersistOnRouteChange: section.shouldPersistOnRouteChange ?? provider.shouldPersistOnRouteChange, alwaysRenderExpandable: section.alwaysRenderExpandable ?? provider.alwaysRenderExpandable, }); diff --git a/src/app/shared/menu/menu.structure.ts b/src/app/shared/menu/menu.structure.ts index 81103ce182..0541d110e0 100644 --- a/src/app/shared/menu/menu.structure.ts +++ b/src/app/shared/menu/menu.structure.ts @@ -53,7 +53,7 @@ function processProviderType(providers: Provider[], menuID: string, providerType const childProviderTypes = (providerType as any).childProviderTypes; childProviderTypes.forEach((childProviderType, childIndex: number) => { - processProviderType(providers, menuID, childProviderType, childIndex, `${menuID}_${index}`, hasSubProviders); + processProviderType(providers, menuID, childProviderType, childIndex, `${menuID}_${index}_0`, hasSubProviders); }); processProviderType(providers, menuID, providerPart, index, parentID, true); 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 e95aa80103..40ab500268 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 @@ -58,4 +58,12 @@ export abstract class AbstractExpandableMenuProvider extends AbstractMenuProvide }) ); } + + protected getAutomatedSectionIdForTopSection(): string { + return this.getAutomatedSectionId(0); + } + protected getAutomatedSectionIdForSubsection(indexOfSubSectionInProvider: number): string { + return `${this.menuProviderId}_0_${indexOfSubSectionInProvider};` + } + } diff --git a/src/app/shared/menu/providers/item-claim.menu.ts b/src/app/shared/menu/providers/item-claim.menu.ts index 06f26a773d..e61872a704 100644 --- a/src/app/shared/menu/providers/item-claim.menu.ts +++ b/src/app/shared/menu/providers/item-claim.menu.ts @@ -81,7 +81,7 @@ export class ClaimMenuProvider extends DSpaceObjectPageMenuProvider { this.translate.get('researcherprofile.success.claim.body'), ); this.authorizationService.invalidateAuthorizationsRequestCache(); - this.menuService.hideMenuSection(MenuID.DSO_EDIT, this.menuProviderId); + this.menuService.hideMenuSection(MenuID.DSO_EDIT, this.getAutomatedSectionId(0)); } else { this.notificationsService.error( this.translate.get('researcherprofile.error.claim.title'), From 3f7c42c51e15ad0799fc4f64ac8e3c250847e3a6 Mon Sep 17 00:00:00 2001 From: Yury Bondarenko Date: Fri, 7 Feb 2025 13:46:22 +0100 Subject: [PATCH 14/32] Menu sections should be inactive by default If an expandable section in the navbar is acive by default, it will expand whn the page loads --- src/app/shared/menu/menu-provider.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/shared/menu/menu-provider.service.ts b/src/app/shared/menu/menu-provider.service.ts index 2ef19010c1..99021b8941 100644 --- a/src/app/shared/menu/menu-provider.service.ts +++ b/src/app/shared/menu/menu-provider.service.ts @@ -178,7 +178,7 @@ export class MenuProviderService { id: section.id ?? `${provider.menuProviderId}_${index}`, parentID: section.parentID ?? provider.parentID, index: provider.index, - active: section.active ?? true, + active: section.active ?? false, shouldPersistOnRouteChange: section.shouldPersistOnRouteChange ?? provider.shouldPersistOnRouteChange, alwaysRenderExpandable: section.alwaysRenderExpandable ?? provider.alwaysRenderExpandable, }); From fbffcca945c6b6d9e86cf9a6e7e6cbd6d181a28c Mon Sep 17 00:00:00 2001 From: Yury Bondarenko Date: Fri, 7 Feb 2025 16:35:17 +0100 Subject: [PATCH 15/32] Resolve post-merge issues - Menu providers weren't included because main configuration is no longer a module - Route definitions didn't get merged because they're no longer modules - Removed old resolver & service (they're providers now) --- src/app/app-routes.ts | 2 - src/app/app.config.ts | 5 + .../collection-page/collection-page-routes.ts | 25 +- .../community-page/community-page-routes.ts | 25 +- src/app/item-page/item-page-routes.ts | 33 +- src/app/menuResolver.ts | 21 -- .../dso-edit-menu-resolver.service.ts | 324 ------------------ .../statistics-page/statistics-page-routes.ts | 13 + 8 files changed, 35 insertions(+), 413 deletions(-) delete mode 100644 src/app/menuResolver.ts delete mode 100644 src/app/shared/dso-page/dso-edit-menu-resolver.service.ts diff --git a/src/app/app-routes.ts b/src/app/app-routes.ts index 51101f5a2d..db71c866f0 100644 --- a/src/app/app-routes.ts +++ b/src/app/app-routes.ts @@ -34,7 +34,6 @@ import { forgotPasswordCheckGuard } from './core/rest-property/forgot-password-c import { ServerCheckGuard } from './core/server-check/server-check.guard'; import { ThemedForbiddenComponent } from './forbidden/themed-forbidden.component'; import { ITEM_MODULE_PATH } from './item-page/item-page-routing-paths'; -import { menuResolver } from './menuResolver'; import { provideSuggestionNotificationsState } from './notifications/provide-suggestion-notifications-state'; import { ThemedPageErrorComponent } from './page-error/themed-page-error.component'; import { ThemedPageInternalServerErrorComponent } from './page-internal-server-error/themed-page-internal-server-error.component'; @@ -50,7 +49,6 @@ export const APP_ROUTES: Route[] = [ path: '', canActivate: [authBlockingGuard], canActivateChild: [ServerCheckGuard], - resolve: [menuResolver], children: [ { path: '', redirectTo: '/home', pathMatch: 'full' }, { diff --git a/src/app/app.config.ts b/src/app/app.config.ts index 77b29206cb..15d7cf4952 100644 --- a/src/app/app.config.ts +++ b/src/app/app.config.ts @@ -38,6 +38,7 @@ import { StoreDevModules } from '../config/store/devtools'; import { environment } from '../environments/environment'; import { EagerThemesModule } from '../themes/eager-themes.module'; import { appEffects } from './app.effects'; +import { MENUS } from './app.menus'; import { appMetaReducers, debugMetaReducers, @@ -156,6 +157,10 @@ export const commonAppConfig: ApplicationConfig = { }, // register the dynamic matcher used by form. MUST be provided by the app module ...DYNAMIC_MATCHER_PROVIDERS, + + // DI-composable menus + ...MENUS, + provideCore(), ], }; diff --git a/src/app/collection-page/collection-page-routes.ts b/src/app/collection-page/collection-page-routes.ts index e20e3ba8af..ffac37c0d1 100644 --- a/src/app/collection-page/collection-page-routes.ts +++ b/src/app/collection-page/collection-page-routes.ts @@ -8,16 +8,14 @@ import { communityBreadcrumbResolver } from '../core/breadcrumbs/community-bread import { i18nBreadcrumbResolver } from '../core/breadcrumbs/i18n-breadcrumb.resolver'; import { ComcolBrowseByComponent } from '../shared/comcol/sections/comcol-browse-by/comcol-browse-by.component'; import { ComcolSearchSectionComponent } from '../shared/comcol/sections/comcol-search-section/comcol-search-section.component'; -import { dsoEditMenuResolver } from '../shared/dso-page/dso-edit-menu.resolver'; -import { LinkMenuItemModel } from '../shared/menu/menu-item/models/link.model'; -import { MenuItemType } from '../shared/menu/menu-item-type.model'; -import { collectionPageResolver } from './collection-page.resolver'; +import { MenuRoute } from '../shared/menu/menu-route.model'; import { collectionPageAdministratorGuard } from './collection-page-administrator.guard'; import { COLLECTION_CREATE_PATH, COLLECTION_EDIT_PATH, ITEMTEMPLATE_PATH, } from './collection-page-routing-paths'; +import { collectionPageResolver } from './collection-page.resolver'; import { CreateCollectionPageComponent } from './create-collection-page/create-collection-page.component'; import { createCollectionPageGuard } from './create-collection-page/create-collection-page.guard'; import { DeleteCollectionPageComponent } from './delete-collection-page/delete-collection-page.component'; @@ -82,8 +80,8 @@ export const ROUTES: Route[] = [ { path: '', component: ThemedCollectionPageComponent, - resolve: { - menu: dsoEditMenuResolver, + data: { + menuRoute: MenuRoute.SIMPLE_COLLECTION_PAGE, }, children: [ { @@ -104,20 +102,5 @@ export const ROUTES: Route[] = [ ], }, ], - data: { - menu: { - public: [{ - id: 'statistics_collection_:id', - active: true, - visible: true, - index: 2, - model: { - type: MenuItemType.LINK, - text: 'menu.section.statistics', - link: 'statistics/collections/:id/', - } as LinkMenuItemModel, - }], - }, - }, }, ]; diff --git a/src/app/community-page/community-page-routes.ts b/src/app/community-page/community-page-routes.ts index 2c8a7942a4..588cc59799 100644 --- a/src/app/community-page/community-page-routes.ts +++ b/src/app/community-page/community-page-routes.ts @@ -7,15 +7,13 @@ import { communityBreadcrumbResolver } from '../core/breadcrumbs/community-bread import { i18nBreadcrumbResolver } from '../core/breadcrumbs/i18n-breadcrumb.resolver'; import { ComcolBrowseByComponent } from '../shared/comcol/sections/comcol-browse-by/comcol-browse-by.component'; import { ComcolSearchSectionComponent } from '../shared/comcol/sections/comcol-search-section/comcol-search-section.component'; -import { dsoEditMenuResolver } from '../shared/dso-page/dso-edit-menu.resolver'; -import { LinkMenuItemModel } from '../shared/menu/menu-item/models/link.model'; -import { MenuItemType } from '../shared/menu/menu-item-type.model'; -import { communityPageResolver } from './community-page.resolver'; +import { MenuRoute } from '../shared/menu/menu-route.model'; import { communityPageAdministratorGuard } from './community-page-administrator.guard'; import { COMMUNITY_CREATE_PATH, COMMUNITY_EDIT_PATH, } from './community-page-routing-paths'; +import { communityPageResolver } from './community-page.resolver'; import { CreateCommunityPageComponent } from './create-community-page/create-community-page.component'; import { createCommunityPageGuard } from './create-community-page/create-community-page.guard'; import { DeleteCommunityPageComponent } from './delete-community-page/delete-community-page.component'; @@ -69,8 +67,8 @@ export const ROUTES: Route[] = [ { path: '', component: ThemedCommunityPageComponent, - resolve: { - menu: dsoEditMenuResolver, + data: { + menuRoute: MenuRoute.SIMPLE_COMMUNITY_PAGE, }, children: [ { @@ -100,20 +98,5 @@ export const ROUTES: Route[] = [ ], }, ], - data: { - menu: { - public: [{ - id: 'statistics_community_:id', - active: true, - visible: true, - index: 2, - model: { - type: MenuItemType.LINK, - text: 'menu.section.statistics', - link: 'statistics/communities/:id/', - } as LinkMenuItemModel, - }], - }, - }, }, ]; diff --git a/src/app/item-page/item-page-routes.ts b/src/app/item-page/item-page-routes.ts index 854d66fabe..1b63ecb520 100644 --- a/src/app/item-page/item-page-routes.ts +++ b/src/app/item-page/item-page-routes.ts @@ -3,23 +3,21 @@ import { Route } from '@angular/router'; import { REQUEST_COPY_MODULE_PATH } from '../app-routing-paths'; import { authenticatedGuard } from '../core/auth/authenticated.guard'; import { itemBreadcrumbResolver } from '../core/breadcrumbs/item-breadcrumb.resolver'; -import { dsoEditMenuResolver } from '../shared/dso-page/dso-edit-menu.resolver'; -import { LinkMenuItemModel } from '../shared/menu/menu-item/models/link.model'; -import { MenuItemType } from '../shared/menu/menu-item-type.model'; +import { MenuRoute } from '../shared/menu/menu-route.model'; import { BitstreamRequestACopyPageComponent } from './bitstreams/request-a-copy/bitstream-request-a-copy-page.component'; import { UploadBitstreamComponent } from './bitstreams/upload/upload-bitstream.component'; import { ThemedFullItemPageComponent } from './full/themed-full-item-page.component'; -import { itemPageResolver } from './item-page.resolver'; import { ITEM_EDIT_PATH, ORCID_PATH, UPLOAD_BITSTREAM_PATH, } from './item-page-routing-paths'; +import { itemPageResolver } from './item-page.resolver'; import { OrcidPageComponent } from './orcid-page/orcid-page.component'; import { orcidPageGuard } from './orcid-page/orcid-page.guard'; import { ThemedItemPageComponent } from './simple/themed-item-page.component'; -import { versionResolver } from './version-page/version.resolver'; import { VersionPageComponent } from './version-page/version-page/version-page.component'; +import { versionResolver } from './version-page/version.resolver'; export const ROUTES: Route[] = [ { @@ -34,16 +32,18 @@ export const ROUTES: Route[] = [ path: '', component: ThemedItemPageComponent, pathMatch: 'full', - resolve: { - menu: dsoEditMenuResolver, + data: { + menuRoute: MenuRoute.SIMPLE_ITEM_PAGE, }, + }, { path: 'full', component: ThemedFullItemPageComponent, - resolve: { - menu: dsoEditMenuResolver, + data: { + menuRoute: MenuRoute.FULL_ITEM_PAGE, }, + }, { path: ITEM_EDIT_PATH, @@ -65,21 +65,6 @@ export const ROUTES: Route[] = [ canActivate: [authenticatedGuard, orcidPageGuard], }, ], - data: { - menu: { - public: [{ - id: 'statistics_item_:id', - active: true, - visible: true, - index: 2, - model: { - type: MenuItemType.LINK, - text: 'menu.section.statistics', - link: 'statistics/items/:id/', - } as LinkMenuItemModel, - }], - }, - }, }, { path: 'version', diff --git a/src/app/menuResolver.ts b/src/app/menuResolver.ts deleted file mode 100644 index 68ad4494dd..0000000000 --- a/src/app/menuResolver.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { inject } from '@angular/core'; -import { - ActivatedRouteSnapshot, - ResolveFn, - RouterStateSnapshot, -} from '@angular/router'; -import { Observable } from 'rxjs'; - -import { MenuResolverService } from './menu-resolver.service'; - - -/** - * Initialize all menus - */ -export const menuResolver: ResolveFn = ( - route: ActivatedRouteSnapshot, - state: RouterStateSnapshot, - menuResolverService: MenuResolverService = inject(MenuResolverService), -): Observable => { - return menuResolverService.resolve(route, state); -}; diff --git a/src/app/shared/dso-page/dso-edit-menu-resolver.service.ts b/src/app/shared/dso-page/dso-edit-menu-resolver.service.ts deleted file mode 100644 index 8c4fd15b7e..0000000000 --- a/src/app/shared/dso-page/dso-edit-menu-resolver.service.ts +++ /dev/null @@ -1,324 +0,0 @@ -import { Injectable } from '@angular/core'; -import { - ActivatedRouteSnapshot, - RouterStateSnapshot, -} from '@angular/router'; -import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; -import { TranslateService } from '@ngx-translate/core'; -import { - combineLatest, - Observable, - of as observableOf, -} from 'rxjs'; -import { - map, - switchMap, -} from 'rxjs/operators'; - -import { getDSORoute } from '../../app-routing-paths'; -import { DSpaceObjectDataService } from '../../core/data/dspace-object-data.service'; -import { AuthorizationDataService } from '../../core/data/feature-authorization/authorization-data.service'; -import { FeatureID } from '../../core/data/feature-authorization/feature-id'; -import { ResearcherProfileDataService } from '../../core/profile/researcher-profile-data.service'; -import { Collection } from '../../core/shared/collection.model'; -import { Community } from '../../core/shared/community.model'; -import { Item } from '../../core/shared/item.model'; -import { - getFirstCompletedRemoteData, - getRemoteDataPayload, -} from '../../core/shared/operators'; -import { CorrectionTypeDataService } from '../../core/submission/correctiontype-data.service'; -import { URLCombiner } from '../../core/url-combiner/url-combiner'; -import { - hasNoValue, - hasValue, - isNotEmpty, -} from '../empty.util'; -import { MenuService } from '../menu/menu.service'; -import { MenuID } from '../menu/menu-id.model'; -import { LinkMenuItemModel } from '../menu/menu-item/models/link.model'; -import { OnClickMenuItemModel } from '../menu/menu-item/models/onclick.model'; -import { MenuItemType } from '../menu/menu-item-type.model'; -import { MenuSection } from '../menu/menu-section.model'; -import { NotificationsService } from '../notifications/notifications.service'; -import { SubscriptionModalComponent } from '../subscriptions/subscription-modal/subscription-modal.component'; -import { DsoVersioningModalService } from './dso-versioning-modal-service/dso-versioning-modal.service'; -import { - DsoWithdrawnReinstateModalService, - REQUEST_REINSTATE, - REQUEST_WITHDRAWN, -} from './dso-withdrawn-reinstate-service/dso-withdrawn-reinstate-modal.service'; - -/** - * Creates the menus for the dspace object pages - */ -@Injectable({ - providedIn: 'root', -}) -export class DSOEditMenuResolverService { - - constructor( - protected dSpaceObjectDataService: DSpaceObjectDataService, - protected menuService: MenuService, - protected authorizationService: AuthorizationDataService, - protected modalService: NgbModal, - protected dsoVersioningModalService: DsoVersioningModalService, - protected researcherProfileService: ResearcherProfileDataService, - protected notificationsService: NotificationsService, - protected translate: TranslateService, - protected dsoWithdrawnReinstateModalService: DsoWithdrawnReinstateModalService, - private correctionTypeDataService: CorrectionTypeDataService, - ) { - } - - /** - * Initialise all dspace object related menus - */ - resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<{ [key: string]: MenuSection[] }> { - let id = route.params.id; - if (hasNoValue(id) && hasValue(route.queryParams.scope)) { - id = route.queryParams.scope; - } - if (hasNoValue(id)) { - // If there's no ID, we're not on a DSO homepage, so pass on any pre-existing menu route data - return observableOf({ ...route.data?.menu }); - } else { - return this.dSpaceObjectDataService.findById(id, true, false).pipe( - getFirstCompletedRemoteData(), - switchMap((dsoRD) => { - if (dsoRD.hasSucceeded) { - const dso = dsoRD.payload; - return combineLatest(this.getDsoMenus(dso, route, state)).pipe( - // Menu sections are retrieved as an array of arrays and flattened into a single array - map((combinedMenus) => [].concat.apply([], combinedMenus)), - map((menus) => this.addDsoUuidToMenuIDs(menus, dso)), - map((menus) => { - return { - ...route.data?.menu, - [MenuID.DSO_EDIT]: menus, - }; - }), - ); - } else { - return observableOf({ ...route.data?.menu }); - } - }), - ); - } - } - - /** - * Return all the menus for a dso based on the route and state - */ - getDsoMenus(dso, route, state): Observable[] { - return [ - this.getItemMenu(dso), - this.getComColMenu(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), - this.correctionTypeDataService.findByItem(dso.uuid, true).pipe( - getFirstCompletedRemoteData(), - getRemoteDataPayload()), - ]).pipe( - map(([canCreateVersion, disableVersioning, versionTooltip, canSynchronizeWithOrcid, canClaimItem, correction]) => { - const isPerson = this.getDsoType(dso) === 'person'; - return [ - { - id: 'orcid-dso', - active: false, - visible: isPerson && canSynchronizeWithOrcid, - model: { - type: MenuItemType.LINK, - text: 'item.page.orcid.tooltip', - link: new URLCombiner(getDSORoute(dso), 'orcid').toString(), - } as LinkMenuItemModel, - icon: 'orcid fab fa-lg', - index: 0, - }, - { - id: 'version-dso', - active: false, - visible: canCreateVersion, - model: { - type: MenuItemType.ONCLICK, - text: versionTooltip, - disabled: disableVersioning, - function: () => { - this.dsoVersioningModalService.openCreateVersionModal(dso); - }, - } as OnClickMenuItemModel, - icon: 'code-branch', - index: 1, - }, - { - id: 'claim-dso', - active: false, - visible: isPerson && canClaimItem, - model: { - type: MenuItemType.ONCLICK, - text: 'item.page.claim.button', - function: () => { - this.claimResearcher(dso); - }, - } as OnClickMenuItemModel, - icon: 'hand-paper', - index: 3, - }, - { - id: 'withdrawn-item', - active: false, - visible: dso.isArchived && correction?.page.some((c) => c.topic === REQUEST_WITHDRAWN), - model: { - type: MenuItemType.ONCLICK, - text:'item.page.withdrawn', - function: () => { - this.dsoWithdrawnReinstateModalService.openCreateWithdrawnReinstateModal(dso, 'request-withdrawn', dso.isArchived); - }, - } as OnClickMenuItemModel, - icon: 'eye-slash', - index: 4, - }, - { - id: 'reinstate-item', - active: false, - visible: dso.isWithdrawn && correction?.page.some((c) => c.topic === REQUEST_REINSTATE), - model: { - type: MenuItemType.ONCLICK, - text:'item.page.reinstate', - function: () => { - this.dsoWithdrawnReinstateModalService.openCreateWithdrawnReinstateModal(dso, 'request-reinstate', dso.isArchived); - }, - } as OnClickMenuItemModel, - icon: 'eye', - index: 5, - }, - ]; - }), - ); - } else { - return observableOf([]); - } - } - - /** - * Get Community/Collection-specific menus - */ - protected getComColMenu(dso): Observable { - if (dso instanceof Community || dso instanceof Collection) { - return combineLatest([ - this.authorizationService.isAuthorized(FeatureID.CanSubscribe, dso.self), - ]).pipe( - map(([canSubscribe]) => { - return [ - { - id: 'subscribe', - active: false, - visible: canSubscribe, - model: { - type: MenuItemType.ONCLICK, - text: 'subscriptions.tooltip', - function: () => { - const modalRef = this.modalService.open(SubscriptionModalComponent); - modalRef.componentInstance.dso = dso; - }, - } as OnClickMenuItemModel, - icon: 'bell', - index: 4, - }, - ]; - }), - ); - } else { - return observableOf([]); - } - } - - /** - * Claim a researcher by creating a profile - * Shows notifications and/or hides the menu section on success/error - */ - protected claimResearcher(dso) { - this.researcherProfileService.createFromExternalSourceAndReturnRelatedItemId(dso.self) - .subscribe((id: string) => { - if (isNotEmpty(id)) { - this.notificationsService.success(this.translate.get('researcherprofile.success.claim.title'), - this.translate.get('researcherprofile.success.claim.body')); - this.authorizationService.invalidateAuthorizationsRequestCache(); - this.menuService.hideMenuSection(MenuID.DSO_EDIT, 'claim-dso-' + dso.uuid); - } else { - this.notificationsService.error( - this.translate.get('researcherprofile.error.claim.title'), - this.translate.get('researcherprofile.error.claim.body')); - } - }); - } - - /** - * Retrieve the dso or entity type for an object to be used in generic messages - */ - protected getDsoType(dso) { - const renderType = dso.getRenderTypes()[0]; - if (typeof renderType === 'string' || renderType instanceof String) { - return renderType.toLowerCase(); - } else { - return dso.type.toString().toLowerCase(); - } - } - - /** - * Add the dso uuid to all provided menu ids and parent ids - */ - protected addDsoUuidToMenuIDs(menus, dso) { - return menus.map((menu) => { - Object.assign(menu, { - id: menu.id + '-' + dso.uuid, - }); - if (hasValue(menu.parentID)) { - Object.assign(menu, { - parentID: menu.parentID + '-' + dso.uuid, - }); - } - return menu; - }); - } - -} diff --git a/src/app/statistics-page/statistics-page-routes.ts b/src/app/statistics-page/statistics-page-routes.ts index 69bcc6b41c..96de7b8273 100644 --- a/src/app/statistics-page/statistics-page-routes.ts +++ b/src/app/statistics-page/statistics-page-routes.ts @@ -41,6 +41,19 @@ export const ROUTES: Route[] = [ component: ThemedItemStatisticsPageComponent, canActivate: [statisticsAdministratorGuard], }, + { + path: `entities/:entityType/:id`, + resolve: { + scope: itemResolver, + breadcrumb: i18nBreadcrumbResolver, + }, + data: { + title: 'statistics.title', + breadcrumbKey: 'statistics', + }, + component: ThemedItemStatisticsPageComponent, + canActivate: [statisticsAdministratorGuard], + }, { path: `collections/:id`, resolve: { From 3fecbc245bc8e3c6c2b6744a431f57dbbb2e77c6 Mon Sep 17 00:00:00 2001 From: Yury Bondarenko Date: Fri, 7 Feb 2025 17:57:42 +0100 Subject: [PATCH 16/32] Fix tests pt. 1 --- ...le-admin-sidebar-section.component.spec.ts | 7 +-- ...-menu-expandable-section.component.spec.ts | 3 +- .../menu-section.component.spec.ts | 4 +- src/app/shared/menu/menu.component.spec.ts | 39 +------------ src/app/shared/menu/menu.component.ts | 45 +++------------ .../shared/menu/providers/browse.menu.spec.ts | 2 +- .../menu/providers/statistics.menu.spec.ts | 27 +++++++++ .../shared/menu/providers/statistics.menu.ts | 55 ++++++++++++------- 8 files changed, 77 insertions(+), 105 deletions(-) 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 0ec0307427..fb227926b6 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 @@ -79,17 +79,14 @@ describe('ExpandableAdminSidebarSectionComponent', () => { describe('when there are no subsections', () => { beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ - imports: [NoopAnimationsModule, TranslateModule.forRoot()], - declarations: [ExpandableAdminSidebarSectionComponent, TestComponent], + imports: [NoopAnimationsModule, TranslateModule.forRoot(), ExpandableAdminSidebarSectionComponent, TestComponent], providers: [ { provide: 'sectionDataProvider', useValue: { icon: iconString, model: {} } }, { provide: MenuService, useValue: menuService }, { provide: CSSVariableService, useClass: CSSVariableServiceStub }, { provide: Router, useValue: new RouterStub() }, ], - }).overrideComponent(ExpandableAdminSidebarSectionComponent, { - }) - .compileComponents(); + }).compileComponents(); })); beforeEach(() => { 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 35eecb60ab..fa11538526 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 @@ -74,8 +74,7 @@ describe('DsoEditMenuExpandableSectionComponent', () => { describe('when there are no subsections', () => { beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ - imports: [TranslateModule.forRoot()], - declarations: [DsoEditMenuExpandableSectionComponent, TestComponent], + imports: [TranslateModule.forRoot(), DsoEditMenuExpandableSectionComponent, TestComponent], providers: [ { provide: 'sectionDataProvider', useValue: dummySection }, { provide: MenuService, useValue: menuService }, diff --git a/src/app/shared/menu/menu-section/menu-section.component.spec.ts b/src/app/shared/menu/menu-section/menu-section.component.spec.ts index 952a1c77ba..1cfaa1dd99 100644 --- a/src/app/shared/menu/menu-section/menu-section.component.spec.ts +++ b/src/app/shared/menu/menu-section/menu-section.component.spec.ts @@ -23,6 +23,7 @@ import { AbstractMenuSectionComponent } from './abstract-menu-section.component' @Component({ selector: 'ds-some-menu-section', template: '', + standalone: true, }) class SomeMenuSectionComponent extends AbstractMenuSectionComponent { constructor( @@ -47,8 +48,7 @@ describe('MenuSectionComponent', () => { active: false, } as any; TestBed.configureTestingModule({ - imports: [TranslateModule.forRoot(), NoopAnimationsModule, SomeMenuSectionComponent], - declarations: [AbstractMenuSectionComponent], + imports: [TranslateModule.forRoot(), NoopAnimationsModule, SomeMenuSectionComponent, AbstractMenuSectionComponent], providers: [ { provide: Injector, useValue: {} }, { provide: MenuService, useClass: MenuServiceStub }, diff --git a/src/app/shared/menu/menu.component.spec.ts b/src/app/shared/menu/menu.component.spec.ts index e128fac3ab..57ad797b37 100644 --- a/src/app/shared/menu/menu.component.spec.ts +++ b/src/app/shared/menu/menu.component.spec.ts @@ -54,6 +54,7 @@ const mockMenuID = 'mock-menuID' as MenuID; // eslint-disable-next-line @angular-eslint/component-selector selector: '', template: '', + standalone: true, }) @rendersSectionForMenu(mockMenuID, true) class TestExpandableMenuComponent { @@ -63,6 +64,7 @@ class TestExpandableMenuComponent { // eslint-disable-next-line @angular-eslint/component-selector selector: '', template: '', + standalone: true, }) @rendersSectionForMenu(mockMenuID, false) class TestMenuComponent { @@ -85,8 +87,6 @@ describe('MenuComponent', () => { visible: true, }; - const mockMenuID = 'mock-menuID' as MenuID; - const mockStatisticSection = { 'id': 'statistics_site', 'active': true, 'visible': true, 'index': 2, 'type': 'statistics', 'model': { 'type': 1, 'text': 'menu.section.statistics', 'link': 'statistics' } }; let authorizationService: AuthorizationDataService; @@ -144,7 +144,7 @@ describe('MenuComponent', () => { }); TestBed.configureTestingModule({ - imports: [TranslateModule.forRoot(), NoopAnimationsModule, RouterTestingModule, MenuComponent, StoreModule.forRoot(authReducer, storeModuleConfig)], + imports: [TranslateModule.forRoot(), NoopAnimationsModule, RouterTestingModule, MenuComponent, StoreModule.forRoot(authReducer, storeModuleConfig), TestExpandableMenuComponent, TestMenuComponent], providers: [ Injector, { provide: ThemeService, useValue: getMockThemeService() }, @@ -152,8 +152,6 @@ describe('MenuComponent', () => { provideMockStore({ initialState }), { provide: AuthorizationDataService, useValue: authorizationService }, { provide: ActivatedRoute, useValue: routeStub }, - TestExpandableMenuComponent, - TestMenuComponent, ], schemas: [NO_ERRORS_SCHEMA], }).overrideComponent(MenuComponent, { @@ -272,35 +270,4 @@ describe('MenuComponent', () => { expect(menuService.collapseMenuPreview).toHaveBeenCalledWith(comp.menuID); })); }); - - describe('when unauthorized statistics', () => { - - beforeEach(() => { - (authorizationService as any).isAuthorized.and.returnValue(observableOf(false)); - fixture.detectChanges(); - }); - - it('should return observable of empty object', done => { - comp.getAuthorizedStatistics(mockStatisticSection).subscribe((res) => { - expect(res).toEqual({}); - done(); - }); - }); - }); - - describe('get authorized statistics', () => { - - beforeEach(() => { - (authorizationService as any).isAuthorized.and.returnValue(observableOf(true)); - fixture.detectChanges(); - }); - - it('should return observable of statistics section menu', done => { - comp.getAuthorizedStatistics(mockStatisticSection).subscribe((res) => { - expect(res).toEqual(mockStatisticSection); - done(); - }); - }); - }); - }); diff --git a/src/app/shared/menu/menu.component.ts b/src/app/shared/menu/menu.component.ts index 97f9d95480..4db4f724e1 100644 --- a/src/app/shared/menu/menu.component.ts +++ b/src/app/shared/menu/menu.component.ts @@ -9,29 +9,26 @@ import { ActivatedRoute } from '@angular/router'; import { BehaviorSubject, Observable, - of as observableOf, Subscription, } from 'rxjs'; import { distinctUntilChanged, map, - mergeMap, switchMap, } from 'rxjs/operators'; import { AuthorizationDataService } from '../../core/data/feature-authorization/authorization-data.service'; -import { FeatureID } from '../../core/data/feature-authorization/feature-id'; import { GenericConstructor } from '../../core/shared/generic-constructor'; import { hasValue, isNotEmptyOperator, } from '../empty.util'; import { ThemeService } from '../theme-support/theme.service'; -import { MenuService } from './menu.service'; import { MenuID } from './menu-id.model'; import { getComponentForMenu } from './menu-section.decorator'; import { MenuSection } from './menu-section.model'; import { AbstractMenuSectionComponent } from './menu-section/abstract-menu-section.component'; +import { MenuService } from './menu.service'; /** * A basic implementation of a MenuComponent @@ -93,8 +90,12 @@ export class MenuComponent implements OnInit, OnDestroy { private activatedRouteLastChild: ActivatedRoute; - constructor(protected menuService: MenuService, protected injector: Injector, public authorizationService: AuthorizationDataService, - public route: ActivatedRoute, protected themeService: ThemeService, + constructor( + protected menuService: MenuService, + protected injector: Injector, + public authorizationService: AuthorizationDataService, + public route: ActivatedRoute, + protected themeService: ThemeService, ) { } @@ -113,12 +114,6 @@ export class MenuComponent implements OnInit, OnDestroy { // if you return an array from a switchMap it will emit each element as a separate event. // So this switchMap is equivalent to a subscribe with a forEach inside switchMap((sections: MenuSection[]) => sections), - mergeMap((section: MenuSection) => { - if (section.id.includes('statistics')) { - return this.getAuthorizedStatistics(section); - } - return observableOf(section); - }), isNotEmptyOperator(), switchMap((section: MenuSection) => this.getSectionComponent(section).pipe( map((component: GenericConstructor) => ({ section, component })), @@ -146,32 +141,6 @@ export class MenuComponent implements OnInit, OnDestroy { } } - /** - * Get section of statistics after checking authorization - */ - getAuthorizedStatistics(section) { - return this.activatedRouteLastChild.data.pipe( - switchMap((data) => { - return this.authorizationService.isAuthorized(FeatureID.CanViewUsageStatistics, this.getObjectUrl(data)).pipe( - map((canViewUsageStatistics: boolean) => { - if (!canViewUsageStatistics) { - return {}; - } else { - return section; - } - })); - }), - ); - } - - /** - * Get statistics route dso data - */ - getObjectUrl(data) { - const object = data.site ? data.site : data.dso?.payload; - return object?._links?.self?.href; - } - /** * Collapse this menu when it's currently expanded, expand it when its currently collapsed * @param {Event} event The user event that triggered this method diff --git a/src/app/shared/menu/providers/browse.menu.spec.ts b/src/app/shared/menu/providers/browse.menu.spec.ts index 663c4825ab..ce395b05ee 100644 --- a/src/app/shared/menu/providers/browse.menu.spec.ts +++ b/src/app/shared/menu/providers/browse.menu.spec.ts @@ -51,7 +51,7 @@ describe('BrowseMenuProvider', () => { let provider: BrowseMenuProvider; - let browseServiceStub = new BrowseServiceStub(); + let browseServiceStub = BrowseServiceStub; beforeEach(() => { spyOn(browseServiceStub, 'getBrowseDefinitions').and.returnValue( diff --git a/src/app/shared/menu/providers/statistics.menu.spec.ts b/src/app/shared/menu/providers/statistics.menu.spec.ts index ee1c0c47ad..09748d4360 100644 --- a/src/app/shared/menu/providers/statistics.menu.spec.ts +++ b/src/app/shared/menu/providers/statistics.menu.spec.ts @@ -1,5 +1,8 @@ +import { inject } from '@angular/core'; import { TestBed } from '@angular/core/testing'; import { TranslateModule } from '@ngx-translate/core'; +import { of as observableOf } from 'rxjs'; +import { AuthorizationDataService } from '../../../core/data/feature-authorization/authorization-data.service'; import { Item } from '../../../core/shared/item.model'; import { ITEM } from '../../../core/shared/item.resource-type'; @@ -34,6 +37,18 @@ describe('StatisticsMenuProvider', () => { }, ]; + const expectedSectionsForItemInvisible: PartialMenuSection[] = [ + { + visible: false, + model: { + type: MenuItemType.LINK, + text: 'menu.section.statistics', + link: `statistics/items/test-item-uuid`, + }, + icon: 'chart-line', + }, + ]; + let provider: StatisticsMenuProvider; const item: Item = Object.assign(new Item(), { @@ -57,13 +72,18 @@ describe('StatisticsMenuProvider', () => { }], }, }); + let authorizationService: AuthorizationDataService; beforeEach(() => { + authorizationService = jasmine.createSpyObj('authorizationService', { + isAuthorized: observableOf(true), + }); TestBed.configureTestingModule({ imports: [TranslateModule.forRoot()], providers: [ StatisticsMenuProvider, + { provide: AuthorizationDataService, useValue: authorizationService }, ], }); provider = TestBed.inject(StatisticsMenuProvider); @@ -86,6 +106,13 @@ describe('StatisticsMenuProvider', () => { done(); }); }); + it('should not return anything if not authorized to view statistics', (done) => { + (TestBed.inject(AuthorizationDataService) as any).isAuthorized.and.returnValue(observableOf(false)); + provider.getSectionsForContext(item).subscribe((sections) => { + expect(sections).toEqual(expectedSectionsForItemInvisible); + done(); + }); + }); }); describe('getRouteContext', () => { diff --git a/src/app/shared/menu/providers/statistics.menu.ts b/src/app/shared/menu/providers/statistics.menu.ts index 37f17cd260..f2ac73b708 100644 --- a/src/app/shared/menu/providers/statistics.menu.ts +++ b/src/app/shared/menu/providers/statistics.menu.ts @@ -12,11 +12,15 @@ import { RouterStateSnapshot, } from '@angular/router'; import { + combineLatest, + map, Observable, of, } from 'rxjs'; import { getDSORoute } from '../../../app-routing-paths'; +import { AuthorizationDataService } from '../../../core/data/feature-authorization/authorization-data.service'; +import { FeatureID } from '../../../core/data/feature-authorization/feature-id'; import { RemoteData } from '../../../core/data/remote-data'; import { DSpaceObject } from '../../../core/shared/dspace-object.model'; import { @@ -34,6 +38,11 @@ import { AbstractRouteContextMenuProvider } from './helper-providers/route-conte */ @Injectable() export class StatisticsMenuProvider extends AbstractRouteContextMenuProvider { + constructor( + protected authorizationService: AuthorizationDataService, + ) { + super(); + } public getRouteContext(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable { let dsoRD: RemoteData = route.data.dso; @@ -51,28 +60,32 @@ export class StatisticsMenuProvider extends AbstractRouteContextMenuProvider { + return combineLatest([ + this.authorizationService.isAuthorized(FeatureID.CanViewUsageStatistics, dso?._links.self.href), + ]).pipe( + map(([authorized]) => { + let link = `statistics`; - let link = `statistics`; + let dsoRoute; + if (hasValue(dso)) { + dsoRoute = getDSORoute(dso); + if (hasValue(dsoRoute)) { + link = `statistics${dsoRoute}`; + } + } - let dsoRoute; - if (hasValue(dso)) { - dsoRoute = getDSORoute(dso); - if (hasValue(dsoRoute)) { - link = `statistics${dsoRoute}`; - } - } - - return of([ - { - visible: true, - model: { - type: MenuItemType.LINK, - text: 'menu.section.statistics', - link, - }, - icon: 'chart-line', - }, - ] as PartialMenuSection[]); + return [ + { + visible: authorized, + model: { + type: MenuItemType.LINK, + text: 'menu.section.statistics', + link, + }, + icon: 'chart-line', + }, + ]; + }), + ); } - } From 516dd99a2641317747acbcc135d0b59408ff96cd Mon Sep 17 00:00:00 2001 From: Yury Bondarenko Date: Fri, 7 Feb 2025 18:31:01 +0100 Subject: [PATCH 17/32] Fix remaining lint issues --- src/app/collection-page/collection-page-routes.ts | 2 +- src/app/community-page/community-page-routes.ts | 2 +- src/app/item-page/item-page-routes.ts | 4 ++-- src/app/shared/menu/menu-provider.model.ts | 2 +- src/app/shared/menu/menu.component.ts | 2 +- src/app/shared/menu/providers/statistics.menu.spec.ts | 3 +-- 6 files changed, 7 insertions(+), 8 deletions(-) diff --git a/src/app/collection-page/collection-page-routes.ts b/src/app/collection-page/collection-page-routes.ts index ffac37c0d1..9df71148e8 100644 --- a/src/app/collection-page/collection-page-routes.ts +++ b/src/app/collection-page/collection-page-routes.ts @@ -9,13 +9,13 @@ import { i18nBreadcrumbResolver } from '../core/breadcrumbs/i18n-breadcrumb.reso import { ComcolBrowseByComponent } from '../shared/comcol/sections/comcol-browse-by/comcol-browse-by.component'; import { ComcolSearchSectionComponent } from '../shared/comcol/sections/comcol-search-section/comcol-search-section.component'; import { MenuRoute } from '../shared/menu/menu-route.model'; +import { collectionPageResolver } from './collection-page.resolver'; import { collectionPageAdministratorGuard } from './collection-page-administrator.guard'; import { COLLECTION_CREATE_PATH, COLLECTION_EDIT_PATH, ITEMTEMPLATE_PATH, } from './collection-page-routing-paths'; -import { collectionPageResolver } from './collection-page.resolver'; import { CreateCollectionPageComponent } from './create-collection-page/create-collection-page.component'; import { createCollectionPageGuard } from './create-collection-page/create-collection-page.guard'; import { DeleteCollectionPageComponent } from './delete-collection-page/delete-collection-page.component'; diff --git a/src/app/community-page/community-page-routes.ts b/src/app/community-page/community-page-routes.ts index 588cc59799..ede79d687d 100644 --- a/src/app/community-page/community-page-routes.ts +++ b/src/app/community-page/community-page-routes.ts @@ -8,12 +8,12 @@ import { i18nBreadcrumbResolver } from '../core/breadcrumbs/i18n-breadcrumb.reso import { ComcolBrowseByComponent } from '../shared/comcol/sections/comcol-browse-by/comcol-browse-by.component'; import { ComcolSearchSectionComponent } from '../shared/comcol/sections/comcol-search-section/comcol-search-section.component'; import { MenuRoute } from '../shared/menu/menu-route.model'; +import { communityPageResolver } from './community-page.resolver'; import { communityPageAdministratorGuard } from './community-page-administrator.guard'; import { COMMUNITY_CREATE_PATH, COMMUNITY_EDIT_PATH, } from './community-page-routing-paths'; -import { communityPageResolver } from './community-page.resolver'; import { CreateCommunityPageComponent } from './create-community-page/create-community-page.component'; import { createCommunityPageGuard } from './create-community-page/create-community-page.guard'; import { DeleteCommunityPageComponent } from './delete-community-page/delete-community-page.component'; diff --git a/src/app/item-page/item-page-routes.ts b/src/app/item-page/item-page-routes.ts index 1b63ecb520..7949c40471 100644 --- a/src/app/item-page/item-page-routes.ts +++ b/src/app/item-page/item-page-routes.ts @@ -7,17 +7,17 @@ import { MenuRoute } from '../shared/menu/menu-route.model'; import { BitstreamRequestACopyPageComponent } from './bitstreams/request-a-copy/bitstream-request-a-copy-page.component'; import { UploadBitstreamComponent } from './bitstreams/upload/upload-bitstream.component'; import { ThemedFullItemPageComponent } from './full/themed-full-item-page.component'; +import { itemPageResolver } from './item-page.resolver'; import { ITEM_EDIT_PATH, ORCID_PATH, UPLOAD_BITSTREAM_PATH, } from './item-page-routing-paths'; -import { itemPageResolver } from './item-page.resolver'; import { OrcidPageComponent } from './orcid-page/orcid-page.component'; import { orcidPageGuard } from './orcid-page/orcid-page.guard'; import { ThemedItemPageComponent } from './simple/themed-item-page.component'; -import { VersionPageComponent } from './version-page/version-page/version-page.component'; import { versionResolver } from './version-page/version.resolver'; +import { VersionPageComponent } from './version-page/version-page/version-page.component'; export const ROUTES: Route[] = [ { diff --git a/src/app/shared/menu/menu-provider.model.ts b/src/app/shared/menu/menu-provider.model.ts index 2117ffe8b6..9f6166519f 100644 --- a/src/app/shared/menu/menu-provider.model.ts +++ b/src/app/shared/menu/menu-provider.model.ts @@ -5,8 +5,8 @@ * * http://www.dspace.org/license/ */ -import { Type } from '@angular/core'; /* eslint-disable max-classes-per-file */ +import { Type } from '@angular/core'; import { ActivatedRouteSnapshot, RouterStateSnapshot, diff --git a/src/app/shared/menu/menu.component.ts b/src/app/shared/menu/menu.component.ts index 4db4f724e1..b3ed0a650d 100644 --- a/src/app/shared/menu/menu.component.ts +++ b/src/app/shared/menu/menu.component.ts @@ -24,11 +24,11 @@ import { isNotEmptyOperator, } from '../empty.util'; import { ThemeService } from '../theme-support/theme.service'; +import { MenuService } from './menu.service'; import { MenuID } from './menu-id.model'; import { getComponentForMenu } from './menu-section.decorator'; import { MenuSection } from './menu-section.model'; import { AbstractMenuSectionComponent } from './menu-section/abstract-menu-section.component'; -import { MenuService } from './menu.service'; /** * A basic implementation of a MenuComponent diff --git a/src/app/shared/menu/providers/statistics.menu.spec.ts b/src/app/shared/menu/providers/statistics.menu.spec.ts index 09748d4360..a50f8d3746 100644 --- a/src/app/shared/menu/providers/statistics.menu.spec.ts +++ b/src/app/shared/menu/providers/statistics.menu.spec.ts @@ -1,9 +1,8 @@ -import { inject } from '@angular/core'; import { TestBed } from '@angular/core/testing'; import { TranslateModule } from '@ngx-translate/core'; import { of as observableOf } from 'rxjs'; -import { AuthorizationDataService } from '../../../core/data/feature-authorization/authorization-data.service'; +import { AuthorizationDataService } from '../../../core/data/feature-authorization/authorization-data.service'; import { Item } from '../../../core/shared/item.model'; import { ITEM } from '../../../core/shared/item.resource-type'; import { createSuccessfulRemoteDataObject } from '../../remote-data.utils'; From f6263f86a4808e80d9db0a2863e8dadea5ec6b25 Mon Sep 17 00:00:00 2001 From: Yana De Pauw Date: Thu, 13 Feb 2025 15:57:02 +0100 Subject: [PATCH 18/32] Fix tests and issue with the getID methods --- src/app/shared/menu/menu-provider.model.ts | 2 +- .../shared/menu/menu-provider.service.spec.ts | 29 ++++++++++++------- .../expandable-menu-provider.ts | 2 +- 3 files changed, 20 insertions(+), 13 deletions(-) diff --git a/src/app/shared/menu/menu-provider.model.ts b/src/app/shared/menu/menu-provider.model.ts index 026ef034bd..1e72d25206 100644 --- a/src/app/shared/menu/menu-provider.model.ts +++ b/src/app/shared/menu/menu-provider.model.ts @@ -144,7 +144,7 @@ export abstract class AbstractMenuProvider implements MenuProvider { abstract getSections(route?: ActivatedRouteSnapshot, state?: RouterStateSnapshot): Observable; protected getAutomatedSectionId(indexOfSectionInProvider: number): string { - return `${this.menuProviderId}_${indexOfSectionInProvider};` + return `${this.menuProviderId}_${indexOfSectionInProvider}`; } } diff --git a/src/app/shared/menu/menu-provider.service.spec.ts b/src/app/shared/menu/menu-provider.service.spec.ts index 41b4d78626..4576a3b1d8 100644 --- a/src/app/shared/menu/menu-provider.service.spec.ts +++ b/src/app/shared/menu/menu-provider.service.spec.ts @@ -38,7 +38,8 @@ describe('MenuProviderService', () => { const router = { events: observableOf(new ResolveEnd(1, 'test-url', 'test-url-after-redirect', { url: 'test-url', - root: {url: [new UrlSegment('test-url', {})], data: {} + root: { + url: [new UrlSegment('test-url', {})], data: {} }, data: {} } as any)) @@ -61,7 +62,7 @@ describe('MenuProviderService', () => { 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 persistentProvider2 = new TestMenuProvider(MenuID.PUBLIC, true, 'provider2', 1, undefined, 'provider1', false, [section, section]); const nonPersistentProvider3 = new TestMenuProvider(MenuID.PUBLIC, false, 'provider3', 2, undefined, undefined, false, [section]); const nonPersistentProvider4 = new TestMenuProvider(MenuID.PUBLIC, false, 'provider4', 3, undefined, 'provider3', false, [section]); const nonPersistentProvider5WithRoutes = new TestMenuProvider(MenuID.PUBLIC, false, 'provider4', 3, [MenuRoute.SIMPLE_COMMUNITY_PAGE, MenuRoute.SIMPLE_COLLECTION_PAGE,], undefined, false, [section]); @@ -69,17 +70,19 @@ describe('MenuProviderService', () => { const listOfProvider = [persistentProvider1, persistentProvider2, nonPersistentProvider3, nonPersistentProvider4, nonPersistentProvider5WithRoutes]; const expectedSection1 = generateAddedSection(persistentProvider1, section); - const expectedSection2 = generateAddedSection(persistentProvider2, section); + const expectedSection21 = generateAddedSection(persistentProvider2, section); + const expectedSection22 = generateAddedSection(persistentProvider2, section, 1); const expectedSection3 = generateAddedSection(nonPersistentProvider3, section); const expectedSection4 = generateAddedSection(nonPersistentProvider4, section); const expectedSection5 = generateAddedSection(nonPersistentProvider5WithRoutes, section); - function generateAddedSection(provider, sectionToAdd) { + function generateAddedSection(provider, sectionToAdd, index = 0) { return { ...sectionToAdd, - id: sectionToAdd.id ?? `${provider.menuProviderId}`, + id: sectionToAdd.id ?? `${provider.menuProviderId}_${index}`, parentID: sectionToAdd.parentID ?? provider.parentID, index: sectionToAdd.index ?? provider.index, + active: false, shouldPersistOnRouteChange: sectionToAdd.shouldPersistOnRouteChange ?? provider.shouldPersistOnRouteChange, alwaysRenderExpandable: sectionToAdd.alwaysRenderExpandable ?? provider.alwaysRenderExpandable, }; @@ -106,7 +109,8 @@ describe('MenuProviderService', () => { menuProviderService.initPersistentMenus(); expect(menuService.addSection).toHaveBeenCalledWith(persistentProvider1.menuID, expectedSection1); - expect(menuService.addSection).toHaveBeenCalledWith(persistentProvider2.menuID, expectedSection2); + expect(menuService.addSection).toHaveBeenCalledWith(persistentProvider2.menuID, expectedSection21); + expect(menuService.addSection).toHaveBeenCalledWith(persistentProvider2.menuID, expectedSection22); expect(menuService.addSection).not.toHaveBeenCalledWith(nonPersistentProvider3.menuID, expectedSection3); expect(menuService.addSection).not.toHaveBeenCalledWith(nonPersistentProvider4.menuID, expectedSection4); expect(menuService.addSection).not.toHaveBeenCalledWith(nonPersistentProvider5WithRoutes.menuID, expectedSection5); @@ -115,7 +119,7 @@ describe('MenuProviderService', () => { 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 = {data:{}}; + const route = {data: {}}; const state = {url: 'test-url'}; menuProviderService.resolveRouteMenus(route as any, state as any).subscribe(); @@ -124,7 +128,8 @@ describe('MenuProviderService', () => { 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).not.toHaveBeenCalledWith(persistentProvider2.menuID, expectedSection21); + expect(menuService.addSection).not.toHaveBeenCalledWith(persistentProvider2.menuID, expectedSection22); expect(menuService.addSection).toHaveBeenCalledWith(nonPersistentProvider3.menuID, expectedSection3); expect(menuService.addSection).toHaveBeenCalledWith(nonPersistentProvider4.menuID, expectedSection4); expect(menuService.addSection).not.toHaveBeenCalledWith(nonPersistentProvider5WithRoutes.menuID, expectedSection5); @@ -133,7 +138,7 @@ describe('MenuProviderService', () => { 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 = {data:{ menuRoute: MenuRoute.SIMPLE_COMMUNITY_PAGE}}; + const route = {data: {menuRoute: MenuRoute.SIMPLE_COMMUNITY_PAGE}}; const state = {url: `xxxx/${COMMUNITY_MODULE_PATH}/xxxxxx`}; menuProviderService.resolveRouteMenus(route as any, state as any).subscribe(); @@ -142,7 +147,8 @@ describe('MenuProviderService', () => { 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).not.toHaveBeenCalledWith(persistentProvider2.menuID, expectedSection21); + expect(menuService.addSection).not.toHaveBeenCalledWith(persistentProvider2.menuID, expectedSection22); expect(menuService.addSection).toHaveBeenCalledWith(nonPersistentProvider3.menuID, expectedSection3); expect(menuService.addSection).toHaveBeenCalledWith(nonPersistentProvider4.menuID, expectedSection4); expect(menuService.addSection).toHaveBeenCalledWith(nonPersistentProvider5WithRoutes.menuID, expectedSection5); @@ -158,7 +164,8 @@ describe('MenuProviderService', () => { 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).not.toHaveBeenCalledWith(persistentProvider2.menuID, expectedSection21); + expect(menuService.addSection).not.toHaveBeenCalledWith(persistentProvider2.menuID, expectedSection22); expect(menuService.addSection).toHaveBeenCalledWith(nonPersistentProvider3.menuID, expectedSection3); expect(menuService.addSection).toHaveBeenCalledWith(nonPersistentProvider4.menuID, expectedSection4); expect(menuService.addSection).not.toHaveBeenCalledWith(nonPersistentProvider5WithRoutes.menuID, expectedSection5); 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 40ab500268..2b773fa24a 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 @@ -63,7 +63,7 @@ export abstract class AbstractExpandableMenuProvider extends AbstractMenuProvide return this.getAutomatedSectionId(0); } protected getAutomatedSectionIdForSubsection(indexOfSubSectionInProvider: number): string { - return `${this.menuProviderId}_0_${indexOfSubSectionInProvider};` + return `${this.menuProviderId}_0_${indexOfSubSectionInProvider}`; } } From 276452e4b9eedda288008a3807d1c0c6ccdfc479 Mon Sep 17 00:00:00 2001 From: Yana De Pauw Date: Fri, 14 Feb 2025 11:08:47 +0100 Subject: [PATCH 19/32] Update DSO menu to also work for subpaths of the DSO and add missing messages --- .../helper-providers/dso.menu.spec.ts | 4 +-- .../providers/helper-providers/dso.menu.ts | 10 +++++-- .../shared/menu/providers/statistics.menu.ts | 28 +++++-------------- src/assets/i18n/en.json5 | 20 +++++++++++++ 4 files changed, 37 insertions(+), 25 deletions(-) diff --git a/src/app/shared/menu/providers/helper-providers/dso.menu.spec.ts b/src/app/shared/menu/providers/helper-providers/dso.menu.spec.ts index 5d214da5f0..63ed018248 100644 --- a/src/app/shared/menu/providers/helper-providers/dso.menu.spec.ts +++ b/src/app/shared/menu/providers/helper-providers/dso.menu.spec.ts @@ -83,7 +83,7 @@ describe('DSpaceObjectPageMenuProvider', () => { }); }); - it('return undefined when no DSO is present on the current route', (done) => { + it('return the first parent DSO when no DSO is present on the current route', (done) => { const route = { data: {}, parent: { @@ -96,7 +96,7 @@ describe('DSpaceObjectPageMenuProvider', () => { } as any; provider.getRouteContext(route, undefined).subscribe((dso) => { - expect(dso).toBeUndefined(); + expect(dso).toEqual(item); done(); }); }); 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 4736e65f41..2cd32a7a7e 100644 --- a/src/app/shared/menu/providers/helper-providers/dso.menu.ts +++ b/src/app/shared/menu/providers/helper-providers/dso.menu.ts @@ -8,9 +8,9 @@ import { ActivatedRouteSnapshot, RouterStateSnapshot, } from '@angular/router'; import { Observable, of } from 'rxjs'; import { DSpaceObject } from '../../../../core/shared/dspace-object.model'; +import { hasNoValue, hasValue } from '../../../empty.util'; 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 @@ -21,7 +21,13 @@ export abstract class DSpaceObjectPageMenuProvider extends AbstractRouteContextM * Retrieve the dso from the current route data */ public getRouteContext(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable { - const dsoRD: RemoteData = route.data.dso; + let dsoRD: RemoteData = route.data.dso; + // Check if one of the parent routes has a DSO + while (hasValue(route.parent) && hasNoValue(dsoRD)) { + route = route.parent; + dsoRD = route.data.dso; + } + if (hasValue(dsoRD) && dsoRD.hasSucceeded && hasValue(dsoRD.payload)) { return of(dsoRD.payload); } else { diff --git a/src/app/shared/menu/providers/statistics.menu.ts b/src/app/shared/menu/providers/statistics.menu.ts index 356f2f53d2..7bc1083e03 100644 --- a/src/app/shared/menu/providers/statistics.menu.ts +++ b/src/app/shared/menu/providers/statistics.menu.ts @@ -7,16 +7,13 @@ */ import { Injectable } from '@angular/core'; -import { ActivatedRouteSnapshot, RouterStateSnapshot, } from '@angular/router'; import { Observable, of, } from 'rxjs'; -import { hasNoValue, hasValue } from '../../empty.util'; +import { hasValue } from '../../empty.util'; import { MenuItemType } from '../menu-item-type.model'; 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'; - +import { DSpaceObjectPageMenuProvider } from './helper-providers/dso.menu'; /** * Menu provider to create the statistics menu section depending on the page it is on @@ -24,22 +21,7 @@ import { getDSORoute } from '../../../app-routing-paths'; * In all other cases the menu section will contain a link to the repository wide statistics */ @Injectable() -export class StatisticsMenuProvider extends AbstractRouteContextMenuProvider { - - public getRouteContext(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable { - let dsoRD: RemoteData = route.data.dso; - // Check if one of the parent routes has a DSO - while (hasValue(route.parent) && hasNoValue(dsoRD)) { - route = route.parent; - dsoRD = route.data.dso; - } - - if (hasValue(dsoRD) && dsoRD.hasSucceeded && hasValue(dsoRD.payload)) { - return of(dsoRD.payload); - } else { - return of(undefined); - } - } +export class StatisticsMenuProvider extends DSpaceObjectPageMenuProvider { public getSectionsForContext(dso: DSpaceObject): Observable { @@ -66,4 +48,8 @@ export class StatisticsMenuProvider extends AbstractRouteContextMenuProvider Date: Fri, 14 Feb 2025 13:11:15 +0100 Subject: [PATCH 20/32] Fix merge issues and tests - Migrate create-report sections to a new provider - Fix menu component test - Add dso option sections to com/col sub paths - Fix issue with breadcrumbs on the collection page --- src/app/app-routes.ts | 2 +- src/app/app.menus.ts | 8 ++ .../collection-page/collection-page-routes.ts | 5 +- .../community-page/community-page-routes.ts | 10 +- src/app/shared/menu/menu-route.model.ts | 3 + src/app/shared/menu/menu.component.spec.ts | 3 + .../menu/providers/create-report.menu.spec.ts | 100 ++++++++++++++++++ .../menu/providers/create-report.menu.ts | 93 ++++++++++++++++ .../helper-providers/dso.menu.spec.ts | 16 +-- .../providers/helper-providers/dso.menu.ts | 5 +- .../shared/menu/providers/statistics.menu.ts | 12 ++- 11 files changed, 240 insertions(+), 17 deletions(-) create mode 100644 src/app/shared/menu/providers/create-report.menu.spec.ts create mode 100644 src/app/shared/menu/providers/create-report.menu.ts diff --git a/src/app/app-routes.ts b/src/app/app-routes.ts index db71c866f0..25324a66be 100644 --- a/src/app/app-routes.ts +++ b/src/app/app-routes.ts @@ -106,7 +106,7 @@ export const APP_ROUTES: Route[] = [ path: COLLECTION_MODULE_PATH, loadChildren: () => import('./collection-page/collection-page-routes') .then((m) => m.ROUTES), - data: { showBreadcrumbs: false, enableRSS: true }, + data: { enableRSS: true }, canActivate: [endUserAgreementCurrentUserGuard], }, { diff --git a/src/app/app.menus.ts b/src/app/app.menus.ts index b5bf4f167f..46dba503a9 100644 --- a/src/app/app.menus.ts +++ b/src/app/app.menus.ts @@ -13,6 +13,7 @@ import { AdminSearchMenuProvider } from './shared/menu/providers/admin-search.me import { BrowseMenuProvider } from './shared/menu/providers/browse.menu'; import { SubscribeMenuProvider } from './shared/menu/providers/comcol-subscribe.menu'; import { CommunityListMenuProvider } from './shared/menu/providers/community-list.menu'; +import { CreateReportMenuProvider } from './shared/menu/providers/create-report.menu'; import { CurationMenuProvider } from './shared/menu/providers/curation.menu'; import { DSpaceObjectEditMenuProvider } from './shared/menu/providers/dso-edit.menu'; import { DsoOptionMenuProvider } from './shared/menu/providers/dso-option.menu'; @@ -42,6 +43,7 @@ export const MENUS = buildMenuStructure({ ImportMenuProvider, ExportMenuProvider, AccessControlMenuProvider, + CreateReportMenuProvider, AdminSearchMenuProvider, RegistriesMenuProvider, CurationMenuProvider, @@ -54,11 +56,17 @@ export const MENUS = buildMenuStructure({ DsoOptionMenuProvider.withSubs([ SubscribeMenuProvider.onRoute( MenuRoute.SIMPLE_COMMUNITY_PAGE, + MenuRoute.SIMPLE_COMMUNITY_BROWSE_PAGE, + MenuRoute.SIMPLE_COMMUNITY_SUBCOLCOM_PAGE, MenuRoute.SIMPLE_COLLECTION_PAGE, + MenuRoute.SIMPLE_COLLECTION_BROWSE_PAGE, ), DSpaceObjectEditMenuProvider.onRoute( MenuRoute.SIMPLE_COMMUNITY_PAGE, + MenuRoute.SIMPLE_COMMUNITY_BROWSE_PAGE, + MenuRoute.SIMPLE_COMMUNITY_SUBCOLCOM_PAGE, MenuRoute.SIMPLE_COLLECTION_PAGE, + MenuRoute.SIMPLE_COLLECTION_BROWSE_PAGE, MenuRoute.SIMPLE_ITEM_PAGE, MenuRoute.FULL_ITEM_PAGE, ), diff --git a/src/app/collection-page/collection-page-routes.ts b/src/app/collection-page/collection-page-routes.ts index 9df71148e8..6e368e5953 100644 --- a/src/app/collection-page/collection-page-routes.ts +++ b/src/app/collection-page/collection-page-routes.ts @@ -97,7 +97,10 @@ export const ROUTES: Route[] = [ resolve: { breadcrumb: browseByI18nBreadcrumbResolver, }, - data: { breadcrumbKey: 'browse.metadata' }, + data: { + breadcrumbKey: 'browse.metadata', + menuRoute: MenuRoute.SIMPLE_COLLECTION_BROWSE_PAGE, + }, }, ], }, diff --git a/src/app/community-page/community-page-routes.ts b/src/app/community-page/community-page-routes.ts index ede79d687d..4295ee62a1 100644 --- a/src/app/community-page/community-page-routes.ts +++ b/src/app/community-page/community-page-routes.ts @@ -83,7 +83,10 @@ export const ROUTES: Route[] = [ resolve: { breadcrumb: i18nBreadcrumbResolver, }, - data: { breadcrumbKey: 'community.subcoms-cols' }, + data: { + breadcrumbKey: 'community.subcoms-cols', + menuRoute: MenuRoute.SIMPLE_COMMUNITY_SUBCOLCOM_PAGE, + }, }, { path: 'browse/:id', @@ -93,7 +96,10 @@ export const ROUTES: Route[] = [ resolve: { breadcrumb: browseByI18nBreadcrumbResolver, }, - data: { breadcrumbKey: 'browse.metadata' }, + data: { + breadcrumbKey: 'browse.metadata', + menuRoute: MenuRoute.SIMPLE_COMMUNITY_BROWSE_PAGE, + }, }, ], }, diff --git a/src/app/shared/menu/menu-route.model.ts b/src/app/shared/menu/menu-route.model.ts index db0fe03169..ac65c03805 100644 --- a/src/app/shared/menu/menu-route.model.ts +++ b/src/app/shared/menu/menu-route.model.ts @@ -3,7 +3,10 @@ */ export enum MenuRoute { SIMPLE_COMMUNITY_PAGE = 'simple-community-page', + SIMPLE_COMMUNITY_BROWSE_PAGE = 'simple-community-browse-page', + SIMPLE_COMMUNITY_SUBCOLCOM_PAGE = 'simple-community-subcolcom-page', SIMPLE_COLLECTION_PAGE = 'simple-collection-page', + SIMPLE_COLLECTION_BROWSE_PAGE = 'simple-collection-browse-page', SIMPLE_ITEM_PAGE = 'simple-item-page', FULL_ITEM_PAGE = 'full-item-page', } diff --git a/src/app/shared/menu/menu.component.spec.ts b/src/app/shared/menu/menu.component.spec.ts index 57ad797b37..59aaccb6f8 100644 --- a/src/app/shared/menu/menu.component.spec.ts +++ b/src/app/shared/menu/menu.component.spec.ts @@ -125,6 +125,7 @@ describe('MenuComponent', () => { id: 'section1', active: false, visible: true, + alwaysRenderExpandable: false, model: { type: MenuItemType.LINK, text: 'test', @@ -190,6 +191,7 @@ describe('MenuComponent', () => { id: 'section1', active: false, visible: true, + alwaysRenderExpandable: false, model: { type: MenuItemType.LINK, text: 'test', @@ -201,6 +203,7 @@ describe('MenuComponent', () => { parentID: 'section1', active: false, visible: true, + alwaysRenderExpandable: false, model: { type: MenuItemType.LINK, text: 'test', diff --git a/src/app/shared/menu/providers/create-report.menu.spec.ts b/src/app/shared/menu/providers/create-report.menu.spec.ts new file mode 100644 index 0000000000..0e2a8841d9 --- /dev/null +++ b/src/app/shared/menu/providers/create-report.menu.spec.ts @@ -0,0 +1,100 @@ +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ + +import { TestBed } from '@angular/core/testing'; +import { of as observableOf } from 'rxjs'; + +import { ConfigurationDataService } from '../../../core/data/configuration-data.service'; +import { AuthorizationDataService } from '../../../core/data/feature-authorization/authorization-data.service'; +import { FeatureID } from '../../../core/data/feature-authorization/feature-id'; +import { ConfigurationProperty } from '../../../core/shared/configuration-property.model'; +import { createSuccessfulRemoteDataObject$ } from '../../remote-data.utils'; +import { AuthorizationDataServiceStub } from '../../testing/authorization-service.stub'; +import { ConfigurationDataServiceStub } from '../../testing/configuration-data.service.stub'; +import { LinkMenuItemModel } from '../menu-item/models/link.model'; +import { TextMenuItemModel } from '../menu-item/models/text.model'; +import { MenuItemType } from '../menu-item-type.model'; +import { PartialMenuSection } from '../menu-provider.model'; +import { CreateReportMenuProvider } from './create-report.menu'; + +describe('CreateReportMenuProvider', () => { + const expectedTopSection: PartialMenuSection = { + visible: true, + model: { + type: MenuItemType.TEXT, + text: 'menu.section.reports', + } as TextMenuItemModel, + icon: 'file-alt', + }; + + const expectedSubSections: PartialMenuSection[] = [ + { + visible: true, + model: { + type: MenuItemType.LINK, + text: 'menu.section.reports.collections', + link: '/admin/reports/collections', + } as LinkMenuItemModel, + icon: 'user-check', + }, + { + visible: true, + model: { + type: MenuItemType.LINK, + text: 'menu.section.reports.queries', + link: '/admin/reports/queries', + } as LinkMenuItemModel, + icon: 'user-check', + }, + ]; + + let provider: CreateReportMenuProvider; + let authorizationServiceStub = new AuthorizationDataServiceStub(); + let configurationDataService = new ConfigurationDataServiceStub(); + + beforeEach(() => { + spyOn(authorizationServiceStub, 'isAuthorized').and.callFake((id: FeatureID) => { + if (id === FeatureID.CanManageGroups) { + return observableOf(false); + } else { + return observableOf(true); + } + }); + + spyOn(configurationDataService, 'findByPropertyName').and.callFake((property: string) => { + return createSuccessfulRemoteDataObject$(Object.assign({}, new ConfigurationProperty(), { values: ['true'] })); + }); + + TestBed.configureTestingModule({ + providers: [ + CreateReportMenuProvider, + { provide: AuthorizationDataService, useValue: authorizationServiceStub }, + { provide: ConfigurationDataService, useValue: configurationDataService }, + ], + }); + provider = TestBed.inject(CreateReportMenuProvider); + }); + + it('should be created', () => { + expect(provider).toBeTruthy(); + }); + + it('getTopSection should return expected menu section', (done) => { + provider.getTopSection().subscribe((section) => { + expect(section).toEqual(expectedTopSection); + done(); + }); + }); + + it('getSubSections should return expected menu sections', (done) => { + provider.getSubSections().subscribe((sections) => { + expect(sections).toEqual(expectedSubSections); + done(); + }); + }); +}); diff --git a/src/app/shared/menu/providers/create-report.menu.ts b/src/app/shared/menu/providers/create-report.menu.ts new file mode 100644 index 0000000000..0b326d9355 --- /dev/null +++ b/src/app/shared/menu/providers/create-report.menu.ts @@ -0,0 +1,93 @@ +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ + +import { Injectable } from '@angular/core'; +import { + combineLatest as observableCombineLatest, + Observable, +} from 'rxjs'; +import { map } from 'rxjs/operators'; + +import { ConfigurationDataService } from '../../../core/data/configuration-data.service'; +import { AuthorizationDataService } from '../../../core/data/feature-authorization/authorization-data.service'; +import { FeatureID } from '../../../core/data/feature-authorization/feature-id'; +import { RemoteData } from '../../../core/data/remote-data'; +import { ConfigurationProperty } from '../../../core/shared/configuration-property.model'; +import { getFirstCompletedRemoteData } from '../../../core/shared/operators'; +import { LinkMenuItemModel } from '../menu-item/models/link.model'; +import { TextMenuItemModel } from '../menu-item/models/text.model'; +import { MenuItemType } from '../menu-item-type.model'; +import { PartialMenuSection } from '../menu-provider.model'; +import { AbstractExpandableMenuProvider } from './helper-providers/expandable-menu-provider'; + +/** + * Menu provider to create the report menu sections + */ +@Injectable() +export class CreateReportMenuProvider extends AbstractExpandableMenuProvider { + constructor( + protected authorizationService: AuthorizationDataService, + protected configurationDataService: ConfigurationDataService, + ) { + super(); + } + + getSubSections(): Observable { + return observableCombineLatest([ + this.configurationDataService.findByPropertyName('contentreport.enable').pipe( + getFirstCompletedRemoteData(), + map((res: RemoteData) => res.hasSucceeded && res.payload && res.payload.values[0] === 'true'), + ), + this.authorizationService.isAuthorized(FeatureID.AdministratorOf), + ]).pipe( + map(([reportEnabled, isSiteAdmin]: [boolean, boolean]) => { + return [ + /* Collections Report */ + { + visible: isSiteAdmin && reportEnabled, + model: { + type: MenuItemType.LINK, + text: 'menu.section.reports.collections', + link: '/admin/reports/collections', + } as LinkMenuItemModel, + icon: 'user-check', + }, + /* Queries Report */ + { + visible: isSiteAdmin && reportEnabled, + model: { + type: MenuItemType.LINK, + text: 'menu.section.reports.queries', + link: '/admin/reports/queries', + } as LinkMenuItemModel, + icon: 'user-check', + }, + ]; + })); + } + + getTopSection(): Observable { + return observableCombineLatest([ + this.configurationDataService.findByPropertyName('contentreport.enable').pipe( + getFirstCompletedRemoteData(), + map((res: RemoteData) => res.hasSucceeded && res.payload && res.payload.values[0] === 'true'), + ), + this.authorizationService.isAuthorized(FeatureID.AdministratorOf), + ]).pipe( + map(([reportEnabled, isSiteAdmin]: [boolean, boolean]) => { + return { + visible: isSiteAdmin && reportEnabled, + model: { + type: MenuItemType.TEXT, + text: 'menu.section.reports', + } as TextMenuItemModel, + icon: 'file-alt', + }; + })); + } +} diff --git a/src/app/shared/menu/providers/helper-providers/dso.menu.spec.ts b/src/app/shared/menu/providers/helper-providers/dso.menu.spec.ts index 38f59a430c..0270d38715 100644 --- a/src/app/shared/menu/providers/helper-providers/dso.menu.spec.ts +++ b/src/app/shared/menu/providers/helper-providers/dso.menu.spec.ts @@ -14,7 +14,7 @@ describe('DSpaceObjectPageMenuProvider', () => { const item: Item = Object.assign(new Item(), { uuid: 'test-item-uuid', type: ITEM.value, - _links: {self: {href: 'self-link'}}, + _links: { self: { href: 'self-link' } }, metadata: { 'dc.title': [{ 'value': 'Untyped Item', @@ -25,7 +25,7 @@ describe('DSpaceObjectPageMenuProvider', () => { const item2: Item = Object.assign(new Item(), { uuid: 'test-item2-uuid', type: ITEM.value, - _links: {self: {href: 'self-link'}}, + _links: { self: { href: 'self-link' } }, metadata: { 'dc.title': [{ 'value': 'Untyped Item 2', @@ -36,7 +36,7 @@ describe('DSpaceObjectPageMenuProvider', () => { const person: Item = Object.assign(new Item(), { uuid: 'test-uuid', type: ITEM.value, - _links: {self: {href: 'self-link'}}, + _links: { self: { href: 'self-link' } }, metadata: { 'dc.title': [{ 'value': 'Person Entity', @@ -50,7 +50,7 @@ describe('DSpaceObjectPageMenuProvider', () => { const collection: Collection = Object.assign(new Collection(), { uuid: 'test-collection-uuid', type: COLLECTION.value, - _links: {self: {href: 'self-link'}}, + _links: { self: { href: 'self-link' } }, metadata: { 'dc.title': [{ 'value': 'Collection', @@ -75,7 +75,7 @@ describe('DSpaceObjectPageMenuProvider', () => { describe('getRouteContext', () => { it('should get the dso from the route', (done) => { - const route = {data: {dso: createSuccessfulRemoteDataObject(item)}} as any; + const route = { data: { dso: createSuccessfulRemoteDataObject(item) } } as any; provider.getRouteContext(route, undefined).subscribe((dso) => { expect(dso).toEqual(item); @@ -89,8 +89,8 @@ describe('DSpaceObjectPageMenuProvider', () => { parent: { data: {}, parent: { - data: {dso: createSuccessfulRemoteDataObject(item)}, - parent: {data: {dso: createSuccessfulRemoteDataObject(item2)}}, + data: { dso: createSuccessfulRemoteDataObject(item) }, + parent: { data: { dso: createSuccessfulRemoteDataObject(item2) } }, }, }, } as any; @@ -101,7 +101,7 @@ describe('DSpaceObjectPageMenuProvider', () => { }); }); it('should return undefined when no dso is found in the route', (done) => { - const route = {data: {}, parent: {data: {}, parent: {data: {}, parent: {data: {}}}}} as any; + const route = { data: {}, parent: { data: {}, parent: { data: {}, parent: { data: {} } } } } as any; provider.getRouteContext(route, undefined).subscribe((dso) => { expect(dso).toBeUndefined(); 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 f19b1cbe53..6440a8340e 100644 --- a/src/app/shared/menu/providers/helper-providers/dso.menu.ts +++ b/src/app/shared/menu/providers/helper-providers/dso.menu.ts @@ -16,7 +16,10 @@ import { import { RemoteData } from '../../../../core/data/remote-data'; import { DSpaceObject } from '../../../../core/shared/dspace-object.model'; -import { hasNoValue, hasValue } from '../../../empty.util'; +import { + hasNoValue, + hasValue, +} from '../../../empty.util'; import { AbstractRouteContextMenuProvider } from './route-context.menu'; /** diff --git a/src/app/shared/menu/providers/statistics.menu.ts b/src/app/shared/menu/providers/statistics.menu.ts index bf7cb2c3a1..b7bbd96e29 100644 --- a/src/app/shared/menu/providers/statistics.menu.ts +++ b/src/app/shared/menu/providers/statistics.menu.ts @@ -7,15 +7,19 @@ */ import { Injectable } from '@angular/core'; -import { combineLatest, map, Observable, } from 'rxjs'; +import { + combineLatest, + map, + Observable, +} from 'rxjs'; +import { getDSORoute } from '../../../app-routing-paths'; import { AuthorizationDataService } from '../../../core/data/feature-authorization/authorization-data.service'; import { FeatureID } from '../../../core/data/feature-authorization/feature-id'; -import { hasValue, } from '../../empty.util'; +import { DSpaceObject } from '../../../core/shared/dspace-object.model'; +import { hasValue } from '../../empty.util'; import { MenuItemType } from '../menu-item-type.model'; import { PartialMenuSection } from '../menu-provider.model'; -import { DSpaceObject } from '../../../core/shared/dspace-object.model'; -import { getDSORoute } from '../../../app-routing-paths'; import { DSpaceObjectPageMenuProvider } from './helper-providers/dso.menu'; /** From 59481abd7f19793a0c94905f85cb433e59feffdc Mon Sep 17 00:00:00 2001 From: Yana De Pauw Date: Mon, 17 Feb 2025 11:15:46 +0100 Subject: [PATCH 21/32] Remove circulary dependency --- src/app/shared/menu/menu.structure.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/app/shared/menu/menu.structure.ts b/src/app/shared/menu/menu.structure.ts index 0541d110e0..cb74af8a69 100644 --- a/src/app/shared/menu/menu.structure.ts +++ b/src/app/shared/menu/menu.structure.ts @@ -8,7 +8,6 @@ import { InjectionToken, Provider, Type, } from '@angular/core'; import { MenuID } from './menu-id.model'; import { AbstractMenuProvider, MenuProviderTypeWithOptions } from './menu-provider.model'; -import { MenuProviderService } from './menu-provider.service'; import { hasValue, isNotEmpty } from '../empty.util'; import { MenuRoute } from './menu-route.model'; @@ -24,7 +23,6 @@ type MenuStructure = { */ export function buildMenuStructure(structure: MenuStructure): Provider[] { const providers: Provider[] = [ - MenuProviderService, ]; Object.entries(structure).forEach(([menuID, providerTypes]) => { From ea53111aa11ca496c1eb5b992a837691045ad30f Mon Sep 17 00:00:00 2001 From: Yana De Pauw Date: Mon, 17 Feb 2025 12:57:47 +0100 Subject: [PATCH 22/32] Remove circulary dependency --- src/app/shared/menu/menu.structure.spec.ts | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/app/shared/menu/menu.structure.spec.ts b/src/app/shared/menu/menu.structure.spec.ts index f62ef03cd1..51ab2b2afe 100644 --- a/src/app/shared/menu/menu.structure.spec.ts +++ b/src/app/shared/menu/menu.structure.spec.ts @@ -4,7 +4,6 @@ import { NewMenuProvider } from './providers/new.menu'; import { DsoOptionMenuProvider } from './providers/dso-option.menu'; import { SubscribeMenuProvider } from './providers/comcol-subscribe.menu'; 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'; @@ -103,16 +102,15 @@ describe('buildMenuStructure', () => { 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); + expect(result.length).toEqual(orderedProviderTypeList.length * 2); }); 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); + expect((result[index * 2] as any).deps).toEqual([provider]); + expect(result[index * 2 + 1]).toEqual(provider); }); }); }); From 8b9af3b04cdac76af688ad02b85268581058c84c Mon Sep 17 00:00:00 2001 From: Yana De Pauw Date: Mon, 17 Feb 2025 12:59:58 +0100 Subject: [PATCH 23/32] Remove circulary dependency --- ...enu-section-Index.model.ts => menu-section-index.model.ts} | 0 src/app/shared/menu/menu.structure.spec.ts | 4 ++-- 2 files changed, 2 insertions(+), 2 deletions(-) rename src/app/shared/menu/{menu-section-Index.model.ts => menu-section-index.model.ts} (100%) diff --git a/src/app/shared/menu/menu-section-Index.model.ts b/src/app/shared/menu/menu-section-index.model.ts similarity index 100% rename from src/app/shared/menu/menu-section-Index.model.ts rename to src/app/shared/menu/menu-section-index.model.ts diff --git a/src/app/shared/menu/menu.structure.spec.ts b/src/app/shared/menu/menu.structure.spec.ts index 51ab2b2afe..c11b18590e 100644 --- a/src/app/shared/menu/menu.structure.spec.ts +++ b/src/app/shared/menu/menu.structure.spec.ts @@ -100,12 +100,12 @@ describe('buildMenuStructure', () => { ]; - it('should have a double amount of objects with an additional service after the processing', () => { + it('should have a double amount of objects after the processing', () => { const result = buildMenuStructure(providerStructure); expect(result.length).toEqual(orderedProviderTypeList.length * 2); }); - it('should return a list with the MenuProviderService and then a resolved provider and provider type for each provider in the provided structure', () => { + it('should return a list with a resolved provider and provider type for each provider in the provided structure', () => { const result = buildMenuStructure(providerStructure); orderedProviderTypeList.forEach((provider, index) => { From bb02acf13e4e640066e753600d66012c7395757e Mon Sep 17 00:00:00 2001 From: Yana De Pauw Date: Fri, 21 Feb 2025 13:23:54 +0100 Subject: [PATCH 24/32] Fix id related issues and add accessibility handle --- src/app/shared/menu/menu-provider.model.ts | 1 + src/app/shared/menu/menu-section.model.ts | 5 +++++ src/app/shared/menu/menu.structure.ts | 6 +++++- .../expandable-menu-provider.spec.ts | 12 ++++++------ .../helper-providers/expandable-menu-provider.ts | 13 +++++++------ .../helper-providers/route-context.menu.spec.ts | 2 +- 6 files changed, 25 insertions(+), 14 deletions(-) diff --git a/src/app/shared/menu/menu-provider.model.ts b/src/app/shared/menu/menu-provider.model.ts index 1e72d25206..c17c5e207e 100644 --- a/src/app/shared/menu/menu-provider.model.ts +++ b/src/app/shared/menu/menu-provider.model.ts @@ -19,6 +19,7 @@ import { MenuRoute } from './menu-route.model'; */ export interface PartialMenuSection { id?: string; + accessibilityHandle?: string; visible: boolean; model: MenuItemModels; parentID?: string; diff --git a/src/app/shared/menu/menu-section.model.ts b/src/app/shared/menu/menu-section.model.ts index a91d75302d..8462e27fd8 100644 --- a/src/app/shared/menu/menu-section.model.ts +++ b/src/app/shared/menu/menu-section.model.ts @@ -19,6 +19,11 @@ export interface MenuSection { */ id: string; + /** + * Accessibility handle that can be used to find a specific menu in the html + */ + accessibilityHandle?: string; + /** * Whether this section should be visible. */ diff --git a/src/app/shared/menu/menu.structure.ts b/src/app/shared/menu/menu.structure.ts index cb74af8a69..cd8ff84433 100644 --- a/src/app/shared/menu/menu.structure.ts +++ b/src/app/shared/menu/menu.structure.ts @@ -83,7 +83,11 @@ function addProviderToList(providers: Provider[], providerType: Type { type: MenuItemType.TEXT, text: 'sub.section.test.1', }, - id: `${MenuID.ADMIN}_1_0`, - parentID: `${MenuID.ADMIN}_1`, + id: `${MenuID.ADMIN}_1_0_0`, + parentID: `${MenuID.ADMIN}_1_0`, alwaysRenderExpandable: false, }, { @@ -69,8 +69,8 @@ describe('AbstractExpandableMenuProvider', () => { type: MenuItemType.TEXT, text: 'sub.section.test.2', }, - id: `${MenuID.ADMIN}_1_1`, - parentID: `${MenuID.ADMIN}_1`, + id: `${MenuID.ADMIN}_1_0_1`, + parentID: `${MenuID.ADMIN}_1_0`, alwaysRenderExpandable: false, }, { @@ -80,9 +80,9 @@ describe('AbstractExpandableMenuProvider', () => { text: 'top.section.test', }, icon: 'file-import', - id: `${MenuID.ADMIN}_1`, + id: `${MenuID.ADMIN}_1_0`, alwaysRenderExpandable: true, - } + }, ]; let provider: AbstractExpandableMenuProvider; 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 2b773fa24a..f769b4987c 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 @@ -36,13 +36,14 @@ export abstract class AbstractExpandableMenuProvider extends AbstractMenuProvide this.getSubSections(), ]).pipe( map(( - [partialTopSection, partialSubSections]: [PartialMenuSection, PartialMenuSection[]] + [partialTopSection, partialSubSections]: [PartialMenuSection, PartialMenuSection[]], ) => { + const parentID = partialTopSection.id ?? this.getAutomatedSectionIdForTopSection(); const subSections = partialSubSections.map((partialSub, index) => { return { ...partialSub, - id: partialSub.id ?? `${this.menuProviderId}_${index}`, - parentID: this.menuProviderId, + id: partialSub.id ?? this.getAutomatedSectionIdForSubsection(index), + parentID: parentID, alwaysRenderExpandable: false, }; }); @@ -51,11 +52,11 @@ export abstract class AbstractExpandableMenuProvider extends AbstractMenuProvide ...subSections, { ...partialTopSection, - id: this.menuProviderId, + id: parentID, alwaysRenderExpandable: this.alwaysRenderExpandable, }, ]; - }) + }), ); } @@ -63,7 +64,7 @@ export abstract class AbstractExpandableMenuProvider extends AbstractMenuProvide return this.getAutomatedSectionId(0); } protected getAutomatedSectionIdForSubsection(indexOfSubSectionInProvider: number): string { - return `${this.menuProviderId}_0_${indexOfSubSectionInProvider}`; + return `${this.getAutomatedSectionIdForTopSection()}_${indexOfSubSectionInProvider}`; } } diff --git a/src/app/shared/menu/providers/helper-providers/route-context.menu.spec.ts b/src/app/shared/menu/providers/helper-providers/route-context.menu.spec.ts index 1120681c05..82539e8b7f 100644 --- a/src/app/shared/menu/providers/helper-providers/route-context.menu.spec.ts +++ b/src/app/shared/menu/providers/helper-providers/route-context.menu.spec.ts @@ -16,7 +16,7 @@ import { CacheableObject } from '../../../../core/cache/cacheable-object.model'; import { ActivatedRouteSnapshot, RouterStateSnapshot } from '@angular/router'; -describe('AbstractExpandableMenuProvider', () => { +describe('AbstractRouteContextMenuProvider', () => { class TestClass extends AbstractRouteContextMenuProvider { getRouteContext(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable { From 43e4f9de499e3cb19d0193f00b2b592b497a7a44 Mon Sep 17 00:00:00 2001 From: Yana De Pauw Date: Fri, 21 Feb 2025 14:36:04 +0100 Subject: [PATCH 25/32] Fix e2e issues --- cypress/e2e/item-statistics.cy.ts | 2 +- .../admin-sidebar-section.component.html | 4 ++-- .../admin-sidebar-section.component.ts | 10 ++++++---- .../expandable-admin-sidebar-section.component.html | 6 +++--- src/app/shared/menu/providers/edit.menu.spec.ts | 1 + src/app/shared/menu/providers/edit.menu.ts | 3 ++- src/app/shared/menu/providers/export.menu.spec.ts | 1 + src/app/shared/menu/providers/export.menu.ts | 3 ++- src/app/shared/menu/providers/new.menu.spec.ts | 1 + src/app/shared/menu/providers/new.menu.ts | 3 ++- 10 files changed, 21 insertions(+), 13 deletions(-) diff --git a/cypress/e2e/item-statistics.cy.ts b/cypress/e2e/item-statistics.cy.ts index 6518f595a9..3456e0207b 100644 --- a/cypress/e2e/item-statistics.cy.ts +++ b/cypress/e2e/item-statistics.cy.ts @@ -7,7 +7,7 @@ describe('Item Statistics Page', () => { it('should load if you click on "Statistics" from an Item/Entity page', () => { cy.visit('/entities/publication/'.concat(Cypress.env('DSPACE_TEST_ENTITY_PUBLICATION'))); cy.get('ds-navbar ds-link-menu-item a[data-test="link-menu-item.menu.section.statistics"]').click(); - cy.location('pathname').should('eq', ITEMSTATISTICSPAGE); + cy.location('pathname').should('eq', '/entities/publication/'.concat(Cypress.env('DSPACE_TEST_ENTITY_PUBLICATION'))); }); it('should contain element ds-item-statistics-page when navigating to an item statistics page', () => { diff --git a/src/app/admin/admin-sidebar/admin-sidebar-section/admin-sidebar-section.component.html b/src/app/admin/admin-sidebar/admin-sidebar-section/admin-sidebar-section.component.html index 30a7a3353b..8d0ac83c32 100644 --- a/src/app/admin/admin-sidebar/admin-sidebar-section/admin-sidebar-section.component.html +++ b/src/app/admin/admin-sidebar/admin-sidebar-section/admin-sidebar-section.component.html @@ -3,7 +3,7 @@ [ngClass]="{ disabled: isDisabled }" role="menuitem" [attr.aria-disabled]="isDisabled" - [attr.aria-labelledby]="adminMenuSectionTitleId(section.id)" + [attr.aria-labelledby]="adminMenuSectionTitleAccessibilityHandle(section)" [routerLink]="itemModel.link" (keyup.space)="navigate($event)" (keyup.enter)="navigate($event)" @@ -14,7 +14,7 @@