Skip to content

Commit cb70190

Browse files
authored
Merge pull request #2610 from alexandrevryghem/menu-section-improvements_contribute-7.6
Fixed menu not updating when a new sub section is added after rendering has already completed
2 parents 7ad5a5b + 1bbc053 commit cb70190

File tree

3 files changed

+129
-19
lines changed

3 files changed

+129
-19
lines changed

src/app/shared/menu/menu.component.spec.ts

Lines changed: 121 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
1+
// eslint-disable-next-line max-classes-per-file
12
import { ComponentFixture, fakeAsync, TestBed, tick, waitForAsync } from '@angular/core/testing';
23
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
34
import { TranslateModule } from '@ngx-translate/core';
4-
import { ChangeDetectionStrategy, Injector, NO_ERRORS_SCHEMA } from '@angular/core';
5+
import { ChangeDetectionStrategy, Injector, NO_ERRORS_SCHEMA, Component } from '@angular/core';
56
import { MenuService } from './menu.service';
67
import { MenuComponent } from './menu.component';
7-
import { MenuServiceStub } from '../testing/menu-service.stub';
8-
import { of as observableOf } from 'rxjs';
9-
import { Router, ActivatedRoute } from '@angular/router';
8+
import { of as observableOf, BehaviorSubject } from 'rxjs';
9+
import { ActivatedRoute } from '@angular/router';
1010
import { RouterTestingModule } from '@angular/router/testing';
1111
import { MenuSection } from './menu-section.model';
1212
import { MenuID } from './menu-id.model';
@@ -15,14 +15,39 @@ import { AuthorizationDataService } from '../../core/data/feature-authorization/
1515
import { createSuccessfulRemoteDataObject } from '../remote-data.utils';
1616
import { ThemeService } from '../theme-support/theme.service';
1717
import { getMockThemeService } from '../mocks/theme-service.mock';
18+
import { MenuItemType } from './menu-item-type.model';
19+
import { LinkMenuItemModel } from './menu-item/models/link.model';
20+
import { provideMockStore, MockStore } from '@ngrx/store/testing';
21+
import { StoreModule, Store } from '@ngrx/store';
22+
import { authReducer } from '../../core/auth/auth.reducer';
23+
import { storeModuleConfig, AppState } from '../../app.reducer';
24+
import { rendersSectionForMenu } from './menu-section.decorator';
25+
26+
const mockMenuID = 'mock-menuID' as MenuID;
27+
28+
@Component({
29+
// eslint-disable-next-line @angular-eslint/component-selector
30+
selector: '',
31+
template: '',
32+
})
33+
@rendersSectionForMenu(mockMenuID, true)
34+
class TestExpandableMenuComponent {
35+
}
36+
37+
@Component({
38+
// eslint-disable-next-line @angular-eslint/component-selector
39+
selector: '',
40+
template: '',
41+
})
42+
@rendersSectionForMenu(mockMenuID, false)
43+
class TestMenuComponent {
44+
}
1845

1946
describe('MenuComponent', () => {
2047
let comp: MenuComponent;
2148
let fixture: ComponentFixture<MenuComponent>;
2249
let menuService: MenuService;
23-
let router: any;
24-
25-
const mockMenuID = 'mock-menuID' as MenuID;
50+
let store: MockStore;
2651

2752
const mockStatisticSection = { 'id': 'statistics_site', 'active': true, 'visible': true, 'index': 2, 'type': 'statistics', 'model': { 'type': 1, 'text': 'menu.section.statistics', 'link': 'statistics' } };
2853

@@ -48,21 +73,55 @@ describe('MenuComponent', () => {
4873
children: []
4974
};
5075

76+
const initialState = {
77+
menus: {
78+
[mockMenuID]: {
79+
collapsed: true,
80+
id: mockMenuID,
81+
previewCollapsed: true,
82+
sectionToSubsectionIndex: {
83+
section1: [],
84+
},
85+
sections: {
86+
section1: {
87+
id: 'section1',
88+
active: false,
89+
visible: true,
90+
model: {
91+
type: MenuItemType.LINK,
92+
text: 'test',
93+
link: '/test',
94+
} as LinkMenuItemModel,
95+
},
96+
},
97+
visible: true,
98+
},
99+
},
100+
};
101+
51102
beforeEach(waitForAsync(() => {
52103

53104
authorizationService = jasmine.createSpyObj('authorizationService', {
54105
isAuthorized: observableOf(false)
55106
});
56107

57-
TestBed.configureTestingModule({
58-
imports: [TranslateModule.forRoot(), NoopAnimationsModule, RouterTestingModule],
108+
void TestBed.configureTestingModule({
109+
imports: [
110+
TranslateModule.forRoot(),
111+
NoopAnimationsModule,
112+
RouterTestingModule,
113+
StoreModule.forRoot(authReducer, storeModuleConfig),
114+
],
59115
declarations: [MenuComponent],
60116
providers: [
61117
Injector,
62118
{ provide: ThemeService, useValue: getMockThemeService() },
63-
{ provide: MenuService, useClass: MenuServiceStub },
119+
MenuService,
120+
provideMockStore({ initialState }),
64121
{ provide: AuthorizationDataService, useValue: authorizationService },
65122
{ provide: ActivatedRoute, useValue: routeStub },
123+
TestExpandableMenuComponent,
124+
TestMenuComponent,
66125
],
67126
schemas: [NO_ERRORS_SCHEMA]
68127
}).overrideComponent(MenuComponent, {
@@ -74,13 +133,62 @@ describe('MenuComponent', () => {
74133
fixture = TestBed.createComponent(MenuComponent);
75134
comp = fixture.componentInstance; // SearchPageComponent test instance
76135
comp.menuID = mockMenuID;
77-
menuService = (comp as any).menuService;
78-
router = TestBed.inject(Router);
136+
menuService = TestBed.inject(MenuService);
137+
store = TestBed.inject(Store) as MockStore<AppState>;
79138
spyOn(comp as any, 'getSectionDataInjector').and.returnValue(MenuSection);
80-
spyOn(comp as any, 'getSectionComponent').and.returnValue(observableOf({}));
81139
fixture.detectChanges();
82140
});
83141

142+
describe('ngOnInit', () => {
143+
it('should trigger the section observable again when a new sub section has been added', () => {
144+
spyOn(comp.sectionMap$, 'next').and.callThrough();
145+
const hasSubSections = new BehaviorSubject(false);
146+
spyOn(menuService, 'hasSubSections').and.returnValue(hasSubSections.asObservable());
147+
spyOn(store, 'dispatch').and.callThrough();
148+
149+
store.setState({
150+
menus: {
151+
[mockMenuID]: {
152+
collapsed: true,
153+
id: mockMenuID,
154+
previewCollapsed: true,
155+
sectionToSubsectionIndex: {
156+
section1: ['test'],
157+
},
158+
sections: {
159+
section1: {
160+
id: 'section1',
161+
active: false,
162+
visible: true,
163+
model: {
164+
type: MenuItemType.LINK,
165+
text: 'test',
166+
link: '/test',
167+
} as LinkMenuItemModel,
168+
},
169+
test: {
170+
id: 'test',
171+
parentID: 'section1',
172+
active: false,
173+
visible: true,
174+
model: {
175+
type: MenuItemType.LINK,
176+
text: 'test',
177+
link: '/test',
178+
} as LinkMenuItemModel,
179+
}
180+
},
181+
visible: true,
182+
},
183+
},
184+
});
185+
expect(menuService.hasSubSections).toHaveBeenCalled();
186+
hasSubSections.next(true);
187+
188+
expect(comp.sectionMap$.next).toHaveBeenCalled();
189+
});
190+
});
191+
84192
describe('toggle', () => {
85193
beforeEach(() => {
86194
spyOn(menuService, 'toggleMenu');

src/app/shared/menu/menu.component.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ import { GenericConstructor } from '../../core/shared/generic-constructor';
66
import { hasValue, isNotEmptyOperator } from '../empty.util';
77
import { MenuSectionComponent } from './menu-section/menu-section.component';
88
import { getComponentForMenu } from './menu-section.decorator';
9-
import { compareArraysUsingIds } from '../../item-page/simple/item-types/shared/item-relationships-utils';
109
import { MenuSection } from './menu-section.model';
1110
import { MenuID } from './menu-id.model';
1211
import { ActivatedRoute } from '@angular/router';
@@ -86,7 +85,7 @@ export class MenuComponent implements OnInit, OnDestroy {
8685
this.menuCollapsed = this.menuService.isMenuCollapsed(this.menuID);
8786
this.menuPreviewCollapsed = this.menuService.isMenuPreviewCollapsed(this.menuID);
8887
this.menuVisible = this.menuService.isMenuVisible(this.menuID);
89-
this.sections = this.menuService.getMenuTopSections(this.menuID).pipe(distinctUntilChanged(compareArraysUsingIds()));
88+
this.sections = this.menuService.getMenuTopSections(this.menuID);
9089

9190
this.subs.push(
9291
this.sections.pipe(
@@ -103,7 +102,7 @@ export class MenuComponent implements OnInit, OnDestroy {
103102
switchMap((section: MenuSection) => this.getSectionComponent(section).pipe(
104103
map((component: GenericConstructor<MenuSectionComponent>) => ({ section, component }))
105104
)),
106-
distinctUntilChanged((x, y) => x.section.id === y.section.id)
105+
distinctUntilChanged((x, y) => x.section.id === y.section.id && x.component.prototype === y.component.prototype),
107106
).subscribe(({ section, component }) => {
108107
const nextMap = this.sectionMap$.getValue();
109108
nextMap.set(section.id, {

src/app/shared/menu/menu.service.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { Injectable } from '@angular/core';
22
import { createSelector, MemoizedSelector, select, Store } from '@ngrx/store';
33
import { AppState, keySelector } from '../../app.reducer';
44
import { combineLatest as observableCombineLatest, Observable } from 'rxjs';
5-
import { filter, map, switchMap, take } from 'rxjs/operators';
5+
import { distinctUntilChanged, filter, map, switchMap, take } from 'rxjs/operators';
66
import {
77
ActivateMenuSectionAction,
88
AddMenuSectionAction,
@@ -23,6 +23,7 @@ import { MenuSections } from './menu-sections.model';
2323
import { MenuSection } from './menu-section.model';
2424
import { MenuID } from './menu-id.model';
2525
import { ActivatedRoute, NavigationEnd, Router } from '@angular/router';
26+
import { compareArraysUsingIds } from '../../item-page/simple/item-types/shared/item-relationships-utils';
2627

2728
export function menuKeySelector<T>(key: string, selector): MemoizedSelector<MenuState, T> {
2829
return createSelector(selector, (state) => {
@@ -81,8 +82,10 @@ export class MenuService {
8182
return this.store.pipe(
8283
select(menuByIDSelector(menuID)),
8384
select(menuSectionStateSelector),
84-
map((sections: MenuSections) => {
85-
return Object.values(sections)
85+
map((sections: MenuSections) => Object.values(sections)),
86+
distinctUntilChanged(compareArraysUsingIds()),
87+
map((sections: MenuSection[]) => {
88+
return sections
8689
.filter((section: MenuSection) => hasNoValue(section.parentID))
8790
.filter((section: MenuSection) => !mustBeVisible || section.visible);
8891
}

0 commit comments

Comments
 (0)