Support withSubs on the menuproviders in appmenu

Adds support to define child providers for a parent menu using a .withSubs option. This parent menu will always be displayed as an expandable menu.
When no children are visible, the expandable menus will be hidden.
This commit is contained in:
Yana De Pauw
2024-10-11 15:51:34 +02:00
parent 297562249a
commit 005936b18d
21 changed files with 330 additions and 185 deletions

View File

@@ -1,4 +1,4 @@
<div class="sidebar-section" [ngClass]="{'expanded': (expanded | async)}" <div *ngIf="hasSubSections$ | async" class="sidebar-section" [ngClass]="{'expanded': (expanded | async)}"
[@bgColor]="{ [@bgColor]="{
value: ((expanded | async) ? 'endBackground' : 'startBackground'), value: ((expanded | async) ? 'endBackground' : 'startBackground'),
params: {endColor: (sidebarActiveBg | async)}}"> params: {endColor: (sidebarActiveBg | async)}}">

View File

@@ -11,6 +11,7 @@ import { map } from 'rxjs/operators';
import { rendersSectionForMenu } from '../../../shared/menu/menu-section.decorator'; import { rendersSectionForMenu } from '../../../shared/menu/menu-section.decorator';
import { MenuID } from '../../../shared/menu/menu-id.model'; import { MenuID } from '../../../shared/menu/menu-id.model';
import { Router } from '@angular/router'; import { Router } from '@angular/router';
import { isNotEmpty } from '../../../shared/empty.util';
/** /**
* Represents a expandable section in the sidebar * Represents a expandable section in the sidebar
@@ -51,6 +52,12 @@ export class ExpandableAdminSidebarSectionComponent extends AdminSidebarSectionC
*/ */
expanded: Observable<boolean>; expanded: Observable<boolean>;
/**
* Emits true when the top section has subsections, else emits false
*/
hasSubSections$: Observable<boolean>;
constructor( constructor(
@Inject('sectionDataProvider') protected section: MenuSection, @Inject('sectionDataProvider') protected section: MenuSection,
protected menuService: MenuService, protected menuService: MenuService,
@@ -66,6 +73,9 @@ export class ExpandableAdminSidebarSectionComponent extends AdminSidebarSectionC
*/ */
ngOnInit(): void { ngOnInit(): void {
super.ngOnInit(); super.ngOnInit();
this.hasSubSections$ = this.subSections$.pipe(
map((subSections) => isNotEmpty(subSections))
);
this.sidebarActiveBg = this.variableService.getVariable('--ds-admin-sidebar-active-bg'); this.sidebarActiveBg = this.variableService.getVariable('--ds-admin-sidebar-active-bg');
this.sidebarCollapsed = this.menuService.isMenuCollapsed(this.menuID); this.sidebarCollapsed = this.menuService.isMenuCollapsed(this.menuID);
this.sidebarPreviewCollapsed = this.menuService.isMenuPreviewCollapsed(this.menuID); this.sidebarPreviewCollapsed = this.menuService.isMenuPreviewCollapsed(this.menuID);

View File

@@ -31,6 +31,7 @@ import { COMMUNITY_MODULE_PATH } from './community-page/community-page-routing-p
import { COLLECTION_MODULE_PATH } from './collection-page/collection-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 { ENTITY_MODULE_PATH, ITEM_MODULE_PATH } from './item-page/item-page-routing-paths';
import { HOME_PAGE_PATH } from './app-routing-paths'; import { HOME_PAGE_PATH } from './app-routing-paths';
import { DsoOptionMenu } from './shared/menu/providers/dso-option-menu.service';
export const MENUS = buildMenuStructure({ export const MENUS = buildMenuStructure({
[MenuID.PUBLIC]: [ [MenuID.PUBLIC]: [
@@ -53,10 +54,13 @@ export const MENUS = buildMenuStructure({
SystemWideAlertMenuProvider, SystemWideAlertMenuProvider,
], ],
[MenuID.DSO_EDIT]: [ [MenuID.DSO_EDIT]: [
DsoOptionMenu.withSubs([
DSpaceObjectEditMenuProvider.onRoute(COMMUNITY_MODULE_PATH, COLLECTION_MODULE_PATH, ITEM_MODULE_PATH, ENTITY_MODULE_PATH), DSpaceObjectEditMenuProvider.onRoute(COMMUNITY_MODULE_PATH, COLLECTION_MODULE_PATH, ITEM_MODULE_PATH, ENTITY_MODULE_PATH),
VersioningMenuProvider.onRoute(ITEM_MODULE_PATH, ENTITY_MODULE_PATH), VersioningMenuProvider.onRoute(ITEM_MODULE_PATH, ENTITY_MODULE_PATH),
OrcidMenuProvider.onRoute(ITEM_MODULE_PATH, ENTITY_MODULE_PATH), OrcidMenuProvider.onRoute(ITEM_MODULE_PATH, ENTITY_MODULE_PATH),
ClaimMenuProvider.onRoute(ITEM_MODULE_PATH, ENTITY_MODULE_PATH), ClaimMenuProvider.onRoute(ITEM_MODULE_PATH, ENTITY_MODULE_PATH),
// SubscribeMenuProvider,
]),
SubscribeMenuProvider.onRoute(COMMUNITY_MODULE_PATH, COLLECTION_MODULE_PATH), SubscribeMenuProvider.onRoute(COMMUNITY_MODULE_PATH, COLLECTION_MODULE_PATH),
], ],
}); });

View File

@@ -34,9 +34,6 @@
</ds-comcol-page-content> </ds-comcol-page-content>
</header> </header>
<ds-dso-edit-menu></ds-dso-edit-menu> <ds-dso-edit-menu></ds-dso-edit-menu>
<div class="pl-2 space-children-mr">
<ds-dso-page-subscription-button [dso]="collection"></ds-dso-page-subscription-button>
</div>
</div> </div>
<section class="comcol-page-browse-section"> <section class="comcol-page-browse-section">
<!-- Browse-By Links --> <!-- Browse-By Links -->

View File

@@ -21,9 +21,6 @@
</ds-comcol-page-content> </ds-comcol-page-content>
</header> </header>
<ds-dso-edit-menu></ds-dso-edit-menu> <ds-dso-edit-menu></ds-dso-edit-menu>
<div class="pl-2 space-children-mr">
<ds-dso-page-subscription-button [dso]="communityPayload"></ds-dso-page-subscription-button>
</div>
</div> </div>
<section class="comcol-page-browse-section"> <section class="comcol-page-browse-section">

View File

@@ -191,11 +191,7 @@ export abstract class InitService {
this.breadcrumbsService.listenForRouteChanges(); this.breadcrumbsService.listenForRouteChanges();
this.themeService.listenForRouteChanges(); this.themeService.listenForRouteChanges();
// this.menuService.listenForRouteChanges(); // this.menuService.listenForRouteChanges();
this.menuProviderService.listenForRouteChanges().subscribe((done) => { this.menuProviderService.listenForRouteChanges();
Object.values(MenuID).forEach((menuID) => {
this.menuService.buildRouteMenuSections(menuID);
});
})
} }
protected initPersistentMenus(): void { protected initPersistentMenus(): void {

View File

@@ -1,3 +1,4 @@
<div *ngIf="hasSubSections$ | async">
<div class="nav-item dropdown expandable-navbar-section text-md-center" <div class="nav-item dropdown expandable-navbar-section text-md-center"
*ngVar="(active | async) as isActive" *ngVar="(active | async) as isActive"
(keyup.enter)="isActive ? deactivateSection($event) : activateSection($event)" (keyup.enter)="isActive ? deactivateSection($event) : activateSection($event)"
@@ -20,3 +21,4 @@
</ng-container> </ng-container>
</ul> </ul>
</div> </div>
</div>

View File

@@ -3,10 +3,12 @@ import { MenuSection } from '../../shared/menu/menu-section.model';
import { NavbarSectionComponent } from '../navbar-section/navbar-section.component'; import { NavbarSectionComponent } from '../navbar-section/navbar-section.component';
import { MenuService } from '../../shared/menu/menu.service'; import { MenuService } from '../../shared/menu/menu.service';
import { slide } from '../../shared/animations/slide'; 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 { HostWindowService } from '../../shared/host-window.service';
import { rendersSectionForMenu } from '../../shared/menu/menu-section.decorator'; import { rendersSectionForMenu } from '../../shared/menu/menu-section.decorator';
import { MenuID } from '../../shared/menu/menu-id.model'; 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 * Represents an expandable section in the navbar
@@ -24,6 +26,11 @@ export class ExpandableNavbarSectionComponent extends NavbarSectionComponent imp
*/ */
menuID = MenuID.PUBLIC; menuID = MenuID.PUBLIC;
/**
* Emits true when the top section has subsections, else emits false
*/
hasSubSections$: Observable<boolean>;
constructor( constructor(
@Inject('sectionDataProvider') protected section: MenuSection, @Inject('sectionDataProvider') protected section: MenuSection,
protected menuService: MenuService, protected menuService: MenuService,
@@ -35,6 +42,9 @@ export class ExpandableNavbarSectionComponent extends NavbarSectionComponent imp
ngOnInit() { ngOnInit() {
super.ngOnInit(); super.ngOnInit();
this.hasSubSections$ = this.subSections$.pipe(
map((subSections) => isNotEmpty(subSections))
);
} }
/** /**

View File

@@ -1,4 +1,4 @@
<div class="dso-button-menu mb-1" ngbDropdown container="body" placement="bottom-right"> <div *ngIf="hasSubSections$ | async" class="dso-button-menu mb-1" ngbDropdown container="body" placement="bottom-right">
<div class="d-flex flex-row flex-nowrap" <div class="d-flex flex-row flex-nowrap"
[ngbTooltip]="itemModel.text | translate"> [ngbTooltip]="itemModel.text | translate">
<button [attr.aria-label]="itemModel.text | translate" [title]="itemModel.text | translate" class="btn btn-dark btn-sm" ngbDropdownToggle [disabled]="section.model?.disabled"> <button [attr.aria-label]="itemModel.text | translate" [title]="itemModel.text | translate" class="btn btn-dark btn-sm" ngbDropdownToggle [disabled]="section.model?.disabled">

View File

@@ -7,7 +7,7 @@ import { MenuID } from 'src/app/shared/menu/menu-id.model';
import { MenuSection } from 'src/app/shared/menu/menu-section.model'; import { MenuSection } from 'src/app/shared/menu/menu-section.model';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
import { map } from 'rxjs/operators'; import { map } from 'rxjs/operators';
import { hasValue } from '../../../empty.util'; import { hasValue, isNotEmpty } from '../../../empty.util';
/** /**
* Represents an expandable section in the dso edit menus * Represents an expandable section in the dso edit menus
@@ -21,11 +21,27 @@ import { hasValue } from '../../../empty.util';
@rendersSectionForMenu(MenuID.DSO_EDIT, true) @rendersSectionForMenu(MenuID.DSO_EDIT, true)
export class DsoEditMenuExpandableSectionComponent extends AbstractMenuSectionComponent { export class DsoEditMenuExpandableSectionComponent extends AbstractMenuSectionComponent {
/**
* This section resides in the DSO edit menu
*/
menuID: MenuID = MenuID.DSO_EDIT; menuID: MenuID = MenuID.DSO_EDIT;
/**
* The MenuItemModel of the top section
*/
itemModel; itemModel;
/**
* Emits whether one of the subsections contains an icon
*/
renderIcons$: Observable<boolean>; renderIcons$: Observable<boolean>;
/**
* Emits true when the top section has subsections, else emits false
*/
hasSubSections$: Observable<boolean>;
constructor( constructor(
@Inject('sectionDataProvider') protected section: MenuSection, @Inject('sectionDataProvider') protected section: MenuSection,
protected menuService: MenuService, protected menuService: MenuService,
@@ -45,5 +61,10 @@ export class DsoEditMenuExpandableSectionComponent extends AbstractMenuSectionCo
return sections.some(section => hasValue(section.icon)); return sections.some(section => hasValue(section.icon));
}), }),
); );
this.hasSubSections$ = this.subSections$.pipe(
map((subSections) => isNotEmpty(subSections))
);
} }
} }

View File

@@ -28,7 +28,7 @@ export const initialMenusState: MenusState = {
id: MenuID.DSO_EDIT, id: MenuID.DSO_EDIT,
collapsed: true, collapsed: true,
previewCollapsed: true, previewCollapsed: true,
visible: false, visible: true,
sections: {}, sections: {},
sectionToSubsectionIndex: {} sectionToSubsectionIndex: {}
}, },

View File

@@ -9,12 +9,10 @@
import { Inject, Injectable, Injector, Optional, } from '@angular/core'; import { Inject, Injectable, Injector, Optional, } from '@angular/core';
import { ActivatedRoute, ActivatedRouteSnapshot, ResolveEnd, Router, RouterStateSnapshot, } from '@angular/router'; import { ActivatedRoute, ActivatedRouteSnapshot, ResolveEnd, Router, RouterStateSnapshot, } from '@angular/router';
import { combineLatest, map, Observable, } from 'rxjs'; import { combineLatest, map, Observable, } from 'rxjs';
import { filter, find, switchMap, take, tap, } from 'rxjs/operators'; import { filter, find, switchMap, take, } from 'rxjs/operators';
import { v4 as uuidv4 } from 'uuid';
import { hasValue, isNotEmpty } from '../empty.util'; import { hasValue, isNotEmpty } from '../empty.util';
import { MenuID } from './menu-id.model'; import { MenuID } from './menu-id.model';
import { AbstractMenuProvider } from './menu-provider'; import { AbstractMenuProvider, PartialMenuSection } from './menu-provider';
import { MenuSection } from './menu-section.model';
import { MenuState } from './menu-state.model'; import { MenuState } from './menu-state.model';
import { MenuService } from './menu.service'; import { MenuService } from './menu.service';
import { MENU_PROVIDER } from './menu.structure'; import { MENU_PROVIDER } from './menu.structure';
@@ -44,8 +42,8 @@ export class MenuProviderService {
); );
} }
listenForRouteChanges(): Observable<boolean> { listenForRouteChanges() {
return this.router.events.pipe( this.router.events.pipe(
filter(event => event instanceof ResolveEnd), filter(event => event instanceof ResolveEnd),
switchMap((event: ResolveEnd) => { switchMap((event: ResolveEnd) => {
@@ -53,7 +51,11 @@ export class MenuProviderService {
return this.resolveRouteMenus(currentRoute, event.state); return this.resolveRouteMenus(currentRoute, event.state);
}), }),
); ).subscribe((done) => {
Object.values(MenuID).forEach((menuID) => {
this.menuService.buildRouteMenuSections(menuID);
});
});
} }
private getCurrentRoute(route: ActivatedRouteSnapshot): ActivatedRouteSnapshot { private getCurrentRoute(route: ActivatedRouteSnapshot): ActivatedRouteSnapshot {
@@ -71,47 +73,30 @@ export class MenuProviderService {
return combineLatest([ return combineLatest([
...this.providers ...this.providers
.map((provider) => {
return provider;
})
.filter(provider => provider.shouldPersistOnRouteChange) .filter(provider => provider.shouldPersistOnRouteChange)
.map(provider => provider.getSections().pipe( .map(provider => provider.getSections()
tap((sections) => { .pipe(
sections.forEach((section: MenuSection) => { map((sections) => {
this.menuService.addSection(provider.menuID, { return {provider: provider, sections: sections};
...section, }),
id: section.id ?? uuidv4(), )
index: section.index ?? provider.index, )])
shouldPersistOnRouteChange: true, .pipe(
}); switchMap((providerWithSections: { provider: AbstractMenuProvider, sections: PartialMenuSection[] }[]) => {
}); const waitForMenus = providerWithSections.map((providerWithSection: {
provider: AbstractMenuProvider,
sections: PartialMenuSection[]
}, sectionIndex) => {
providerWithSection.sections.forEach((section) => {
this.addSection(providerWithSection, section);
});
return this.waitForMenu$(providerWithSection.provider.menuID);
});
return [waitForMenus];
}), }),
switchMap(() => this.waitForMenu$(provider.menuID)),
)),
// ...this.providers
// .filter(provider => {
// let matchesPath = false;
// if (isNotEmpty(provider.activePaths)) {
// route.url.forEach((urlSegment) => {
// if (provider.activePaths.includes(urlSegment.path)) {
// matchesPath = true;
// }
// })
// }
// return matchesPath;
// })
// .map(provider => provider.getSections(route, state).pipe(
// tap((sections) => {
// sections.forEach((section: MenuSection) => {
// this.menuService.addSection(provider.menuID, {
// ...section,
// id: section.id ?? uuidv4(),
// index: section.index ?? provider.index,
// shouldPersistOnRouteChange: false,
// });
// });
// }),
// switchMap(() => this.waitForMenu$(provider.menuID)),
// ))
]
).pipe(
map(done => done.every(Boolean)), map(done => done.every(Boolean)),
); );
} }
@@ -122,78 +107,75 @@ export class MenuProviderService {
): Observable<boolean> { ): Observable<boolean> {
return combineLatest([ return combineLatest([
...Object.values(MenuID).map((menuID) => { ...Object.values(MenuID).map((menuID) => {
return this.menuService.getNonPersistentMenuSections(menuID) return this.menuService.getNonPersistentMenuSections(menuID).pipe(
.pipe(
take(1), take(1),
map((sections) => { map((sections) => {
return {menuId: menuID, sections: sections}; return {menuId: menuID, sections: sections};
}) }));
); })])
})]).pipe( .pipe(
switchMap((menuSectionsPerMenu) => { switchMap((menuSectionsPerMenu) => {
menuSectionsPerMenu.forEach((menu) => { this.removeNonPersistentSections(menuSectionsPerMenu);
menu.sections.forEach((section) => {
this.menuService.removeSection(menu.menuId, section.id);
});
});
return combineLatest([ return combineLatest([
// ...this.providers
// .filter(provider => isEmpty(provider.activePaths))
// .map(provider => provider.getSections(route.snapshot, state).pipe(
// tap((sections) => {
// sections.forEach((section: MenuSection) => {
// this.menuService.addSection(provider.menuID, {
// ...section,
// id: section.id ?? uuidv4(),
// index: section.index ?? provider.index,
// shouldPersistOnRouteChange: true,
// });
// });
// }),
// switchMap(() => this.waitForMenu$(provider.menuID)),
// )),
...this.providers ...this.providers
.filter(provider => { .filter(provider => {
let shouldUpdate = false; let shouldUpdate = false;
if (!provider.shouldPersistOnRouteChange && isNotEmpty(provider.activePaths)) { if (!provider.shouldPersistOnRouteChange && isNotEmpty(provider.activePaths)) {
provider.activePaths.forEach((path) => { provider.activePaths.forEach((path) => {
if (state.url.includes(path)) { if (state.url.includes(path)) {
shouldUpdate = true; shouldUpdate = true;
} }
}); });
} else { } else if (!provider.shouldPersistOnRouteChange) {
if (!provider.shouldPersistOnRouteChange) {
shouldUpdate = true; shouldUpdate = true;
} }
}
return shouldUpdate; return shouldUpdate;
}) })
.map(provider => provider.getSections(route, state).pipe( .map(provider => provider.getSections(route, state)
tap((sections) => { .pipe(
map((sections) => {
sections.forEach((section: MenuSection) => { return {provider: provider, sections: sections};
this.menuService.addSection(provider.menuID, { }),
...section, )
id: section.id ?? uuidv4(), )
index: section.index ?? provider.index, ]);
shouldPersistOnRouteChange: false, }),
}); switchMap((providerWithSections: { provider: AbstractMenuProvider, sections: PartialMenuSection[] }[]) => {
}); const waitForMenus = providerWithSections.map((providerWithSection: {
provider: AbstractMenuProvider,
sections: PartialMenuSection[]
}, sectionIndex) => {
providerWithSection.sections.forEach((section) => {
this.addSection(providerWithSection, section);
});
return this.waitForMenu$(providerWithSection.provider.menuID);
});
return [waitForMenus];
}), }),
switchMap(() => this.waitForMenu$(provider.menuID)),
))]
).pipe(
map(done => done.every(Boolean)), map(done => done.every(Boolean)),
); );
// }
} }
)
); private addSection(providerWithSection: {
provider: AbstractMenuProvider;
sections: PartialMenuSection[]
}, section: PartialMenuSection) {
this.menuService.addSection(providerWithSection.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,
isExpandable: section.isExpandable ?? providerWithSection.provider.isExpandable,
});
}
private removeNonPersistentSections(menuSectionsPerMenu) {
menuSectionsPerMenu.forEach((menu) => {
menu.sections.forEach((section) => {
this.menuService.removeSection(menu.menuId, section.id);
});
});
} }
} }

View File

@@ -18,7 +18,7 @@ import {
} from 'rxjs'; } from 'rxjs';
import { map } from 'rxjs/operators'; import { map } from 'rxjs/operators';
import { MenuID } from './menu-id.model'; import { MenuID } from './menu-id.model';
import { MenuSection } from './menu-section.model'; import { MenuItemModels, MenuSection } from './menu-section.model';
import { APP_INITIALIZER, Provider, Type } from '@angular/core'; import { APP_INITIALIZER, Provider, Type } from '@angular/core';
import { APP_CONFIG } from '../../../config/app-config.interface'; import { APP_CONFIG } from '../../../config/app-config.interface';
import { TransferState } from '@angular/platform-browser'; import { TransferState } from '@angular/platform-browser';
@@ -26,7 +26,19 @@ import { environment } from '../../../environments/environment';
import { HOME_PAGE_PATH } from '../../app-routing-paths'; import { HOME_PAGE_PATH } from '../../app-routing-paths';
import { MENU_PROVIDER } from './menu.structure'; import { MENU_PROVIDER } from './menu.structure';
export type PartialMenuSection = Omit<MenuSection, 'id' | 'active'>; // export type PartialMenuSection = Omit<MenuSection, 'id' | 'active'>;
export interface PartialMenuSection {
id?: string;
visible: boolean;
model: MenuItemModels;
parentID?: string;
index?: number;
active?: boolean;
shouldPersistOnRouteChange?: boolean;
icon?: string;
isExpandable?: boolean;
}
export interface MenuProvider { export interface MenuProvider {
@@ -37,11 +49,25 @@ export interface MenuProvider {
getSections(route?: ActivatedRouteSnapshot, state?: RouterStateSnapshot): Observable<PartialMenuSection[]>; getSections(route?: ActivatedRouteSnapshot, state?: RouterStateSnapshot): Observable<PartialMenuSection[]>;
} }
export class MenuProviderTypeWithPaths {
providerType: Type<MenuProvider>;
paths: string[];
}
export class MenuProviderTypeWithSubs {
providerType: Type<MenuProvider>;
childProviderTypes: (Type<MenuProvider> | MenuProviderTypeWithPaths)[];
}
export abstract class AbstractMenuProvider implements MenuProvider { export abstract class AbstractMenuProvider implements MenuProvider {
shouldPersistOnRouteChange = true; shouldPersistOnRouteChange = true;
menuID?: MenuID; menuID?: MenuID;
menuProviderId?: string;
index?: number; index?: number;
activePaths?: string[]; activePaths?: string[];
parentID?: string;
isExpandable = false;
abstract getSections(route?: ActivatedRouteSnapshot, state?: RouterStateSnapshot): Observable<PartialMenuSection[]>; abstract getSections(route?: ActivatedRouteSnapshot, state?: RouterStateSnapshot): Observable<PartialMenuSection[]>;

View File

@@ -79,4 +79,6 @@ export interface MenuSection {
* Note that not all menus may render icons. * Note that not all menus may render icons.
*/ */
icon?: string; icon?: string;
isExpandable?: boolean;
} }

View File

@@ -218,7 +218,7 @@ export class MenuComponent implements OnInit, OnDestroy {
private getSectionComponent(section: MenuSection): Observable<GenericConstructor<AbstractMenuSectionComponent>> { private getSectionComponent(section: MenuSection): Observable<GenericConstructor<AbstractMenuSectionComponent>> {
return this.menuService.hasSubSections(this.menuID, section.id).pipe( return this.menuService.hasSubSections(this.menuID, section.id).pipe(
map((expandable: boolean) => { map((expandable: boolean) => {
return getComponentForMenu(this.menuID, expandable, this.themeService.getThemeName()); return getComponentForMenu(this.menuID, expandable || section.isExpandable, this.themeService.getThemeName());
} }
), ),
); );

View File

@@ -2,7 +2,7 @@ import { Injectable } from '@angular/core';
import { createSelector, MemoizedSelector, select, Store } from '@ngrx/store'; import { createSelector, MemoizedSelector, select, Store } from '@ngrx/store';
import { AppState, keySelector } from '../../app.reducer'; import { AppState, keySelector } from '../../app.reducer';
import { combineLatest as observableCombineLatest, Observable } from 'rxjs'; import { combineLatest as observableCombineLatest, Observable } from 'rxjs';
import { filter, map, switchMap, take } from 'rxjs/operators'; import { filter, map, switchMap, take, tap } from 'rxjs/operators';
import { import {
ActivateMenuSectionAction, ActivateMenuSectionAction,
AddMenuSectionAction, AddMenuSectionAction,

View File

@@ -13,13 +13,40 @@ import {
import { MenuID } from './menu-id.model'; import { MenuID } from './menu-id.model';
import { AbstractMenuProvider } from './menu-provider'; import { AbstractMenuProvider } from './menu-provider';
import { MenuProviderService } from './menu-provider.service'; import { MenuProviderService } from './menu-provider.service';
import { hasValue, isNotEmpty } from '../empty.util';
export const MENU_PROVIDER = new InjectionToken<AbstractMenuProvider>('MENU_PROVIDER'); export const MENU_PROVIDER = new InjectionToken<AbstractMenuProvider>('MENU_PROVIDER');
type MenuStructure = { type MenuStructure = {
[key in MenuID]: (Type<AbstractMenuProvider> | {providerType: Type<AbstractMenuProvider>, paths: string[]})[]; [key in MenuID]: (Type<AbstractMenuProvider> | {providerType: Type<AbstractMenuProvider>, paths: string[]} | {providerType: Type<AbstractMenuProvider>, childProviderTypes: any[]})[];
}; };
function resolveProvider(providerType: Type<AbstractMenuProvider> , menuID: string, index: number, paths?: string[], parentID?: string, childProviders? : Type<AbstractMenuProvider>[]) {
return {
provide: MENU_PROVIDER,
multi: true,
useFactory(provider: AbstractMenuProvider): AbstractMenuProvider {
provider.menuID = menuID as MenuID;
provider.index = provider.index ?? index;
if (hasValue(parentID)) {
provider.menuProviderId = `${parentID}_${provider.constructor.name}-${provider.index}`
provider.parentID = parentID;
} else {
provider.menuProviderId = `${provider.constructor.name}-${provider.index}`;
}
if (isNotEmpty(paths)) {
provider.activePaths = paths;
provider.shouldPersistOnRouteChange = false;
}
if (isNotEmpty(childProviders)) {
provider.shouldPersistOnRouteChange = false;
}
return provider;
},
deps: [providerType],
};
}
export function buildMenuStructure(structure: MenuStructure): Provider[] { export function buildMenuStructure(structure: MenuStructure): Provider[] {
const providers: Provider[] = [ const providers: Provider[] = [
MenuProviderService, MenuProviderService,
@@ -29,36 +56,39 @@ export function buildMenuStructure(structure: MenuStructure): Provider[] {
for (const [index, providerType] of providerTypes.entries()) { for (const [index, providerType] of providerTypes.entries()) {
// todo: should complain if not injectable! // todo: should complain if not injectable!
if (providerType.hasOwnProperty('providerType')) { if (providerType.hasOwnProperty('providerType') && providerType.hasOwnProperty('paths')) {
const providerPart = (providerType as any).providerType; const providerPart = (providerType as any).providerType;
const paths = (providerType as any).paths; const paths = (providerType as any).paths;
providers.push(providerPart);
providers.push(resolveProvider(providerPart, menuID, index, paths));
} else if (providerType.hasOwnProperty('providerType') && providerType.hasOwnProperty('childProviderTypes')){
const providerPart = (providerType as any).providerType;
const childProviderList = [];
const childProviderTypes = (providerType as any).childProviderTypes;
childProviderTypes.forEach((childProviderType, childIndex: number) => {
if (childProviderType.hasOwnProperty('providerType') && childProviderType.hasOwnProperty('paths')) {
const childProviderTypePart = (childProviderType as any).providerType;
const paths = (childProviderType as any).paths;
providers.push(childProviderTypePart);
providers.push(resolveProvider(childProviderTypePart, menuID, childIndex, paths, `${providerPart.name}-${index}`))
childProviderList.push(childProviderTypePart);
} else {
providers.push(childProviderType)
providers.push(resolveProvider(childProviderType, menuID, childIndex, undefined, `${providerPart.name}-${index}`))
childProviderList.push(childProviderType);
}
})
providers.push(providerPart); providers.push(providerPart);
providers.push({ providers.push(resolveProvider(providerPart, menuID, index, undefined, undefined, childProviderList));
provide: MENU_PROVIDER,
multi: true,
useFactory(provider: AbstractMenuProvider): AbstractMenuProvider {
provider.menuID = menuID as MenuID;
provider.index = provider.index ?? index;
provider.activePaths = paths;
provider.shouldPersistOnRouteChange = false;
return provider;
},
deps: [providerPart],
});
} else { } else {
providers.push(providerType as Type<AbstractMenuProvider> ); providers.push(providerType as Type<AbstractMenuProvider> );
providers.push({ providers.push(resolveProvider(providerType as Type<AbstractMenuProvider>, menuID, index));
provide: MENU_PROVIDER,
multi: true,
useFactory(provider: AbstractMenuProvider): AbstractMenuProvider {
provider.menuID = menuID as MenuID;
provider.index = provider.index ?? index;
return provider;
},
deps: [providerType],
});
} }
} }
}); });

View File

@@ -35,10 +35,10 @@ export class SubscribeMenuProvider extends DSpaceObjectPageMenuProvider<Communit
super(dsoDataService); super(dsoDataService);
} }
protected isApplicable(dso: Community | Collection): boolean { // protected isApplicable(dso: Community | Collection): boolean {
// @ts-ignore // // @ts-ignore
return dso.type === COMMUNITY.value || dso.type.value === COLLECTION; // return dso?.type === COMMUNITY.value || dso?.type === COLLECTION.value;
} // }
public getSectionsForContext(dso: Community | Collection): Observable<PartialMenuSection[]> { public getSectionsForContext(dso: Community | Collection): Observable<PartialMenuSection[]> {
return combineLatest([ return combineLatest([

View File

@@ -0,0 +1,30 @@
/**
* The contents of this file are subject to the license and copyright
* detailed in the LICENSE and NOTICE files at the root of the source
* tree and available online at
*
* http://www.dspace.org/license/
*/
import { Injectable } from '@angular/core';
import { Observable, of, } from 'rxjs';
import { MenuItemType } from '../menu-item-type.model';
import { AbstractExpandableParentMenuProvider } from './expandable-parent-menu-provider';
import { PartialMenuSection } from '../menu-provider';
@Injectable()
export class DsoOptionMenu extends AbstractExpandableParentMenuProvider {
public getSections(): Observable<PartialMenuSection[]> {
return of([
{
visible: true,
model: {
type: MenuItemType.TEXT,
text: `menu.section.browse_global_communities_and_collections`,
},
icon: 'diagram-project'
},
] as PartialMenuSection[]);
}
}

View File

@@ -17,6 +17,7 @@ import {
AbstractMenuProvider, AbstractMenuProvider,
PartialMenuSection, PartialMenuSection,
} from '../menu-provider'; } from '../menu-provider';
import { Type } from '@angular/core';
export type MenuTopSection = Omit<PartialMenuSection, 'visible'>; export type MenuTopSection = Omit<PartialMenuSection, 'visible'>;
export type MenuSubSection = Omit<PartialMenuSection, 'parentID'>; export type MenuSubSection = Omit<PartialMenuSection, 'parentID'>;
@@ -34,7 +35,6 @@ export abstract class AbstractExpandableMenuProvider extends AbstractMenuProvide
getSections(): Observable<PartialMenuSection[]> { getSections(): Observable<PartialMenuSection[]> {
const full = this.includeSubSections(); const full = this.includeSubSections();
const parentID = uuidv4();
return combineLatest([ return combineLatest([
this.getTopSection(), this.getTopSection(),
@@ -43,10 +43,11 @@ export abstract class AbstractExpandableMenuProvider extends AbstractMenuProvide
map(( map((
[partialTopSection, partialSubSections]: [MenuTopSection, MenuSubSection[]] [partialTopSection, partialSubSections]: [MenuTopSection, MenuSubSection[]]
) => { ) => {
const subSections = partialSubSections.map(partialSub => { const subSections = partialSubSections.map((partialSub, index) => {
return { return {
...partialSub, ...partialSub,
parentID: parentID, id: partialSub.id ?? `${this.menuProviderId}_Sub-${index}`,
parentID: this.menuProviderId,
}; };
}); });
@@ -54,7 +55,7 @@ export abstract class AbstractExpandableMenuProvider extends AbstractMenuProvide
...subSections, ...subSections,
{ {
...partialTopSection, ...partialTopSection,
id: parentID, id: this.menuProviderId,
visible: full ? subSections.some(sub => sub.visible) : this.showWithoutSubsections, visible: full ? subSections.some(sub => sub.visible) : this.showWithoutSubsections,
}, },
]; ];

View File

@@ -0,0 +1,37 @@
/**
* The contents of this file are subject to the license and copyright
* detailed in the LICENSE and NOTICE files at the root of the source
* tree and available online at
*
* http://www.dspace.org/license/
*/
import { combineLatest, Observable, of as observableOf, } from 'rxjs';
import { map } from 'rxjs/operators';
import {
AbstractMenuProvider,
MenuProvider,
MenuProviderTypeWithPaths,
MenuProviderTypeWithSubs,
} from '../menu-provider';
import { Inject, Injector, Optional, Type } from '@angular/core';
import { AbstractExpandableMenuProvider, MenuSubSection } from './expandable-menu-provider';
import { ActivatedRoute, ActivatedRouteSnapshot, Router, RouterStateSnapshot } from '@angular/router';
import { isEmpty } from '../../empty.util';
import { MENU_PROVIDER } from '../menu.structure';
import { MenuService } from '../menu.service';
export abstract class AbstractExpandableParentMenuProvider extends AbstractMenuProvider {
isExpandable = true;
public static withSubs(childProviders: (Type<MenuProvider>| MenuProviderTypeWithPaths)[]) {
if (!AbstractMenuProvider.isPrototypeOf(this)) {
throw new Error(
'onRoute should only be called from concrete subclasses of AbstractMenuProvider'
);
}
const providerType = this as unknown as Type<AbstractMenuProvider>;
return {providerType: providerType, childProviderTypes: childProviders};
}
}