Skip to content

Commit c9654b2

Browse files
authored
Merge pull request #3994 from atmire/refactor-menu-resolvers-9.0
Refactor menu resolvers 9.0
2 parents e0393ba + 39bcaa0 commit c9654b2

File tree

118 files changed

+6054
-2701
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

118 files changed

+6054
-2701
lines changed

cypress/e2e/item-statistics.cy.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ describe('Item Statistics Page', () => {
77
it('should load if you click on "Statistics" from an Item/Entity page', () => {
88
cy.visit('/entities/publication/'.concat(Cypress.env('DSPACE_TEST_ENTITY_PUBLICATION')));
99
cy.get('ds-navbar ds-link-menu-item a[data-test="link-menu-item.menu.section.statistics"]').click();
10-
cy.location('pathname').should('eq', ITEMSTATISTICSPAGE);
10+
cy.location('pathname').should('eq', '/statistics/entities/publication/'.concat(Cypress.env('DSPACE_TEST_ENTITY_PUBLICATION')));
1111
});
1212

1313
it('should contain element ds-item-statistics-page when navigating to an item statistics page', () => {

src/app/admin/admin-sidebar/admin-sidebar-section/admin-sidebar-section.component.html

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
[ngClass]="{ disabled: isDisabled }"
44
role="menuitem"
55
[attr.aria-disabled]="isDisabled"
6-
[attr.aria-labelledby]="adminMenuSectionTitleId(section.id)"
6+
[attr.aria-labelledby]="adminMenuSectionTitleAccessibilityHandle(section)"
77
[routerLink]="itemModel.link"
88
(keyup.space)="navigate($event)"
99
(keyup.enter)="navigate($event)"
@@ -14,7 +14,7 @@
1414
</div>
1515
<div class="sidebar-collapsible-element-outer-wrapper">
1616
<div class="sidebar-collapsible-element-inner-wrapper sidebar-item">
17-
<span [id]="adminMenuSectionTitleId(section.id)" [attr.data-test]="adminMenuSectionTitleId(section.id) | dsBrowserOnly">
17+
<span [id]="adminMenuSectionTitleAccessibilityHandle(section)" [attr.data-test]="adminMenuSectionTitleAccessibilityHandle(section) | dsBrowserOnly">
1818
{{itemModel.text | translate}}
1919
</span>
2020
</div>

src/app/admin/admin-sidebar/admin-sidebar-section/admin-sidebar-section.component.ts

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import { MenuService } from '../../../shared/menu/menu.service';
1616
import { MenuID } from '../../../shared/menu/menu-id.model';
1717
import { LinkMenuItemModel } from '../../../shared/menu/menu-item/models/link.model';
1818
import { MenuSection } from '../../../shared/menu/menu-section.model';
19-
import { MenuSectionComponent } from '../../../shared/menu/menu-section/menu-section.component';
19+
import { AbstractMenuSectionComponent } from '../../../shared/menu/menu-section/abstract-menu-section.component';
2020
import { BrowserOnlyPipe } from '../../../shared/utils/browser-only.pipe';
2121

2222
/**
@@ -30,7 +30,7 @@ import { BrowserOnlyPipe } from '../../../shared/utils/browser-only.pipe';
3030
imports: [NgClass, RouterLink, TranslateModule, BrowserOnlyPipe],
3131

3232
})
33-
export class AdminSidebarSectionComponent extends MenuSectionComponent implements OnInit {
33+
export class AdminSidebarSectionComponent extends AbstractMenuSectionComponent implements OnInit {
3434

3535
/**
3636
* This section resides in the Admin Sidebar
@@ -44,16 +44,17 @@ export class AdminSidebarSectionComponent extends MenuSectionComponent implement
4444
isDisabled: boolean;
4545

4646
constructor(
47-
@Inject('sectionDataProvider') menuSection: MenuSection,
47+
@Inject('sectionDataProvider') protected section: MenuSection,
4848
protected menuService: MenuService,
4949
protected injector: Injector,
5050
protected router: Router,
5151
) {
52-
super(menuSection, menuService, injector);
53-
this.itemModel = menuSection.model as LinkMenuItemModel;
52+
super(menuService, injector);
53+
this.itemModel = section.model as LinkMenuItemModel;
5454
}
5555

5656
ngOnInit(): void {
57+
// todo: should support all menu entries?
5758
this.isDisabled = this.itemModel?.disabled || isEmpty(this.itemModel?.link);
5859
super.ngOnInit();
5960
}
@@ -65,11 +66,13 @@ export class AdminSidebarSectionComponent extends MenuSectionComponent implement
6566
}
6667
}
6768

68-
adminMenuSectionId(sectionId: string) {
69-
return `admin-menu-section-${sectionId}`;
69+
adminMenuSectionId(section: MenuSection) {
70+
const accessibilityHandle = section.accessibilityHandle ?? section.id;
71+
return `admin-menu-section-${accessibilityHandle}`;
7072
}
7173

72-
adminMenuSectionTitleId(sectionId: string) {
73-
return `admin-menu-section-${sectionId}-title`;
74+
adminMenuSectionTitleAccessibilityHandle(section: MenuSection) {
75+
const accessibilityHandle = section.accessibilityHandle ?? section.id;
76+
return `admin-menu-section-${accessibilityHandle}-title`;
7477
}
7578
}
Lines changed: 44 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,49 +1,52 @@
1-
<div [ngClass]="{'expanded': (isExpanded$ | async)}"
2-
[@bgColor]="{
1+
@if (hasSubSections$ | async) {
2+
<div
3+
[ngClass]="{'expanded': (isExpanded$ | async)}"
4+
[@bgColor]="{
35
value: ((isExpanded$ | async) ? 'endBackground' : 'startBackground'),
46
params: {endColor: (sidebarActiveBg$ | async)}
57
}">
6-
<a class="sidebar-section-wrapper"
7-
role="menuitem" tabindex="0"
8-
aria-haspopup="menu"
9-
[attr.aria-controls]="adminMenuSectionId(section.id)"
10-
[attr.aria-expanded]="isExpanded$ | async"
11-
[attr.aria-label]="('menu.section.toggle.' + section.id) | translate"
12-
[class.disabled]="section.model?.disabled"
13-
(click)="toggleSection($event)"
14-
(keyup.space)="toggleSection($event)"
15-
href="javascript:void(0);"
16-
>
17-
<div class="sidebar-fixed-element-wrapper" data-test="sidebar-section-icon" aria-hidden="true">
18-
<i class="fas fa-{{section.icon}} fa-fw"></i>
19-
</div>
20-
<div class="sidebar-collapsible-element-outer-wrapper">
21-
<div class="sidebar-collapsible-element-inner-wrapper sidebar-item toggler-wrapper">
22-
<span [id]="adminMenuSectionTitleId(section.id)" [attr.data-test]="adminMenuSectionTitleId(section.id) | dsBrowserOnly">
23-
<ng-container
24-
*ngComponentOutlet="(sectionMap$ | async).get(section.id).component; injector: (sectionMap$ | async).get(section.id).injector;"></ng-container>
25-
</span>
26-
<i class="fas fa-chevron-right fa-xs" aria-hidden="true"
27-
[@rotate]="(isExpanded$ | async) ? 'expanded' : 'collapsed'"
28-
></i>
8+
<a class="sidebar-section-wrapper"
9+
role="menuitem" tabindex="0"
10+
aria-haspopup="menu"
11+
[attr.aria-controls]="adminMenuSectionId(section)"
12+
[attr.aria-expanded]="isExpanded$ | async"
13+
[attr.aria-label]="('menu.section.toggle.' + section.id) | translate"
14+
[class.disabled]="section.model?.disabled"
15+
(click)="toggleSection($event)"
16+
(keyup.space)="toggleSection($event)"
17+
href="javascript:void(0);"
18+
>
19+
<div class="sidebar-fixed-element-wrapper" data-test="sidebar-section-icon" aria-hidden="true">
20+
<i class="fas fa-{{section.icon ?? 'notdef'}} fa-fw"></i>
2921
</div>
30-
</div>
31-
</a>
32-
@if ((isExpanded$ | async)) {
33-
<div class="sidebar-section-wrapper subsection" @slide>
34-
<div class="sidebar-fixed-element-wrapper"></div>
3522
<div class="sidebar-collapsible-element-outer-wrapper">
36-
<div class="sidebar-collapsible-element-inner-wrapper">
37-
<div class="sidebar-sub-level-item-list" role="menu" [id]="adminMenuSectionId(section.id)" [attr.aria-label]="('menu.section.' + section.id) | translate">
38-
@for (subSection of (subSections$ | async); track subSection) {
39-
<div class="sidebar-item">
40-
<ng-container
41-
*ngComponentOutlet="(sectionMap$ | async).get(subSection.id).component; injector: (sectionMap$ | async).get(subSection.id).injector;"></ng-container>
42-
</div>
43-
}
23+
<div class="sidebar-collapsible-element-inner-wrapper sidebar-item toggler-wrapper">
24+
<span [id]="adminMenuSectionTitleAccessibilityHandle(section)" [attr.data-test]="adminMenuSectionTitleAccessibilityHandle(section) | dsBrowserOnly">
25+
<ng-container
26+
*ngComponentOutlet="(sectionMap$ | async).get(section.id).component; injector: (sectionMap$ | async).get(section.id).injector;"></ng-container>
27+
</span>
28+
<i class="fas fa-chevron-right fa-xs" aria-hidden="true"
29+
[@rotate]="(isExpanded$ | async) ? 'expanded' : 'collapsed'"
30+
></i>
31+
</div>
32+
</div>
33+
</a>
34+
@if ((isExpanded$ | async)) {
35+
<div class="sidebar-section-wrapper subsection" @slide>
36+
<div class="sidebar-fixed-element-wrapper"></div>
37+
<div class="sidebar-collapsible-element-outer-wrapper">
38+
<div class="sidebar-collapsible-element-inner-wrapper">
39+
<div class="sidebar-sub-level-item-list" role="menu" [id]="adminMenuSectionId(section)" [attr.aria-label]="('menu.section.' + section.id) | translate">
40+
@for (subSection of (subSections$ | async); track subSection) {
41+
<div class="sidebar-item">
42+
<ng-container
43+
*ngComponentOutlet="(sectionMap$ | async).get(subSection.id).component; injector: (sectionMap$ | async).get(subSection.id).injector;"></ng-container>
44+
</div>
45+
}
46+
</div>
4447
</div>
4548
</div>
4649
</div>
47-
</div>
48-
}
49-
</div>
50+
}
51+
</div>
52+
}

src/app/admin/admin-sidebar/expandable-admin-sidebar-section/expandable-admin-sidebar-section.component.spec.ts

Lines changed: 76 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { TranslateModule } from '@ngx-translate/core';
1111
import { of as observableOf } from 'rxjs';
1212

1313
import { MenuService } from '../../../shared/menu/menu.service';
14+
import { MenuItemModels } from '../../../shared/menu/menu-section.model';
1415
import { CSSVariableService } from '../../../shared/sass-helper/css-variable.service';
1516
import { CSSVariableServiceStub } from '../../../shared/testing/css-variable-service.stub';
1617
import { MenuServiceStub } from '../../../shared/testing/menu-service.stub';
@@ -22,47 +23,89 @@ describe('ExpandableAdminSidebarSectionComponent', () => {
2223
let fixture: ComponentFixture<ExpandableAdminSidebarSectionComponent>;
2324
const menuService = new MenuServiceStub();
2425
const iconString = 'test';
25-
beforeEach(waitForAsync(() => {
26-
TestBed.configureTestingModule({
27-
imports: [NoopAnimationsModule, TranslateModule.forRoot(), ExpandableAdminSidebarSectionComponent, TestComponent],
28-
providers: [
29-
{ provide: 'sectionDataProvider', useValue: { icon: iconString, model: {} } },
30-
{ provide: MenuService, useValue: menuService },
31-
{ provide: CSSVariableService, useClass: CSSVariableServiceStub },
32-
{ provide: Router, useValue: new RouterStub() },
33-
],
34-
}).compileComponents();
35-
}));
3626

37-
beforeEach(() => {
38-
spyOn(menuService, 'getSubSectionsByParentID').and.returnValue(observableOf([]));
39-
fixture = TestBed.createComponent(ExpandableAdminSidebarSectionComponent);
40-
component = fixture.componentInstance;
41-
spyOn(component as any, 'getMenuItemComponent').and.returnValue(TestComponent);
42-
fixture.detectChanges();
43-
});
4427

45-
it('should create', () => {
46-
expect(component).toBeTruthy();
47-
});
28+
describe('when there are subsections', () => {
29+
beforeEach(waitForAsync(() => {
30+
TestBed.configureTestingModule({
31+
imports: [NoopAnimationsModule, TranslateModule.forRoot(), ExpandableAdminSidebarSectionComponent, TestComponent],
32+
providers: [
33+
{ provide: 'sectionDataProvider', useValue: { icon: iconString, model: {} } },
34+
{ provide: MenuService, useValue: menuService },
35+
{ provide: CSSVariableService, useClass: CSSVariableServiceStub },
36+
{ provide: Router, useValue: new RouterStub() },
37+
],
38+
}).compileComponents();
39+
}));
40+
41+
beforeEach(() => {
42+
spyOn(menuService, 'getSubSectionsByParentID').and.returnValue(observableOf([{
43+
id: 'test',
44+
visible: true,
45+
model: {} as MenuItemModels,
46+
}]));
47+
fixture = TestBed.createComponent(ExpandableAdminSidebarSectionComponent);
48+
component = fixture.componentInstance;
49+
spyOn(component as any, 'getMenuItemComponent').and.returnValue(TestComponent);
50+
fixture.detectChanges();
51+
});
52+
53+
it('should create', () => {
54+
expect(component).toBeTruthy();
55+
});
56+
57+
it('should set the right icon', () => {
58+
const icon = fixture.debugElement.query(By.css('[data-test="sidebar-section-icon"] > i.fas'));
59+
expect(icon.nativeElement.getAttribute('class')).toContain('fa-' + iconString);
60+
});
4861

49-
it('should set the right icon', () => {
50-
const icon = fixture.debugElement.query(By.css('[data-test="sidebar-section-icon"] > i.fas'));
51-
expect(icon.nativeElement.getAttribute('class')).toContain('fa-' + iconString);
62+
describe('when the header text is clicked', () => {
63+
beforeEach(() => {
64+
spyOn(menuService, 'toggleActiveSection');
65+
const sidebarToggler = fixture.debugElement.query(By.css('a.sidebar-section-wrapper'));
66+
sidebarToggler.triggerEventHandler('click', {
67+
preventDefault: () => {/**/
68+
},
69+
});
70+
});
71+
72+
it('should call toggleActiveSection on the menuService', () => {
73+
expect(menuService.toggleActiveSection).toHaveBeenCalled();
74+
});
75+
});
5276
});
5377

54-
describe('when the header text is clicked', () => {
78+
79+
describe('when there are no subsections', () => {
80+
beforeEach(waitForAsync(() => {
81+
TestBed.configureTestingModule({
82+
imports: [NoopAnimationsModule, TranslateModule.forRoot(), ExpandableAdminSidebarSectionComponent, TestComponent],
83+
providers: [
84+
{ provide: 'sectionDataProvider', useValue: { icon: iconString, model: {} } },
85+
{ provide: MenuService, useValue: menuService },
86+
{ provide: CSSVariableService, useClass: CSSVariableServiceStub },
87+
{ provide: Router, useValue: new RouterStub() },
88+
],
89+
}).compileComponents();
90+
}));
91+
5592
beforeEach(() => {
56-
spyOn(menuService, 'toggleActiveSection');
57-
const sidebarToggler = fixture.debugElement.query(By.css('a.sidebar-section-wrapper'));
58-
sidebarToggler.triggerEventHandler('click', {
59-
preventDefault: () => {/**/
60-
},
61-
});
93+
spyOn(menuService, 'getSubSectionsByParentID').and.returnValue(observableOf([]));
94+
fixture = TestBed.createComponent(ExpandableAdminSidebarSectionComponent);
95+
component = fixture.componentInstance;
96+
spyOn(component as any, 'getMenuItemComponent').and.returnValue(TestComponent);
97+
fixture.detectChanges();
98+
});
99+
100+
it('should create', () => {
101+
expect(component).toBeTruthy();
62102
});
63103

64-
it('should call toggleActiveSection on the menuService', () => {
65-
expect(menuService.toggleActiveSection).toHaveBeenCalled();
104+
it('should not contain a section', () => {
105+
const icon = fixture.debugElement.query(By.css('.shortcut-icon'));
106+
expect(icon).toBeNull();
107+
const sidebarToggler = fixture.debugElement.query(By.css('.sidebar-section'));
108+
expect(sidebarToggler).toBeNull();
66109
});
67110
});
68111
});

src/app/admin/admin-sidebar/expandable-admin-sidebar-section/expandable-admin-sidebar-section.component.ts

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,10 @@ import { map } from 'rxjs/operators';
2020
import { bgColor } from '../../../shared/animations/bgColor';
2121
import { rotate } from '../../../shared/animations/rotate';
2222
import { slide } from '../../../shared/animations/slide';
23+
import { isNotEmpty } from '../../../shared/empty.util';
2324
import { MenuService } from '../../../shared/menu/menu.service';
2425
import { MenuID } from '../../../shared/menu/menu-id.model';
26+
import { MenuSection } from '../../../shared/menu/menu-section.model';
2527
import { CSSVariableService } from '../../../shared/sass-helper/css-variable.service';
2628
import { BrowserOnlyPipe } from '../../../shared/utils/browser-only.pipe';
2729
import { AdminSidebarSectionComponent } from '../admin-sidebar-section/admin-sidebar-section.component';
@@ -65,21 +67,30 @@ export class ExpandableAdminSidebarSectionComponent extends AdminSidebarSectionC
6567
*/
6668
isExpanded$: Observable<boolean>;
6769

70+
/**
71+
* Emits true when the top section has subsections, else emits false
72+
*/
73+
hasSubSections$: Observable<boolean>;
74+
75+
6876
constructor(
69-
@Inject('sectionDataProvider') menuSection,
77+
@Inject('sectionDataProvider') protected section: MenuSection,
7078
protected menuService: MenuService,
7179
private variableService: CSSVariableService,
7280
protected injector: Injector,
7381
protected router: Router,
7482
) {
75-
super(menuSection, menuService, injector, router);
83+
super(section, menuService, injector, router);
7684
}
7785

7886
/**
7987
* Set initial values for instance variables
8088
*/
8189
ngOnInit(): void {
8290
super.ngOnInit();
91+
this.hasSubSections$ = this.subSections$.pipe(
92+
map((subSections) => isNotEmpty(subSections)),
93+
);
8394
this.sidebarActiveBg$ = this.variableService.getVariable('--ds-admin-sidebar-active-bg');
8495
this.isSidebarCollapsed$ = this.menuService.isMenuCollapsed(this.menuID);
8596
this.isSidebarPreviewCollapsed$ = this.menuService.isMenuPreviewCollapsed(this.menuID);

src/app/app-routes.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,6 @@ import { forgotPasswordCheckGuard } from './core/rest-property/forgot-password-c
3434
import { ServerCheckGuard } from './core/server-check/server-check.guard';
3535
import { ThemedForbiddenComponent } from './forbidden/themed-forbidden.component';
3636
import { ITEM_MODULE_PATH } from './item-page/item-page-routing-paths';
37-
import { menuResolver } from './menuResolver';
3837
import { provideSuggestionNotificationsState } from './notifications/provide-suggestion-notifications-state';
3938
import { ThemedPageErrorComponent } from './page-error/themed-page-error.component';
4039
import { ThemedPageInternalServerErrorComponent } from './page-internal-server-error/themed-page-internal-server-error.component';
@@ -50,7 +49,6 @@ export const APP_ROUTES: Route[] = [
5049
path: '',
5150
canActivate: [authBlockingGuard],
5251
canActivateChild: [ServerCheckGuard],
53-
resolve: [menuResolver],
5452
children: [
5553
{ path: '', redirectTo: '/home', pathMatch: 'full' },
5654
{

src/app/app.config.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ import { StoreDevModules } from '../config/store/devtools';
3939
import { environment } from '../environments/environment';
4040
import { EagerThemesModule } from '../themes/eager-themes.module';
4141
import { appEffects } from './app.effects';
42+
import { MENUS } from './app.menus';
4243
import {
4344
appMetaReducers,
4445
debugMetaReducers,
@@ -159,6 +160,10 @@ export const commonAppConfig: ApplicationConfig = {
159160
},
160161
// register the dynamic matcher used by form. MUST be provided by the app module
161162
...DYNAMIC_MATCHER_PROVIDERS,
163+
164+
// DI-composable menus
165+
...MENUS,
166+
162167
provideCore(),
163168
],
164169
};

0 commit comments

Comments
 (0)