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
This commit is contained in:
Tim Donohue
2023-11-13 16:29:45 -06:00
committed by GitHub
3 changed files with 129 additions and 19 deletions

View File

@@ -1,12 +1,12 @@
// eslint-disable-next-line max-classes-per-file
import { ComponentFixture, fakeAsync, TestBed, tick, waitForAsync } from '@angular/core/testing'; import { ComponentFixture, fakeAsync, TestBed, tick, waitForAsync } from '@angular/core/testing';
import { NoopAnimationsModule } from '@angular/platform-browser/animations'; import { NoopAnimationsModule } from '@angular/platform-browser/animations';
import { TranslateModule } from '@ngx-translate/core'; import { TranslateModule } from '@ngx-translate/core';
import { ChangeDetectionStrategy, Injector, NO_ERRORS_SCHEMA } from '@angular/core'; import { ChangeDetectionStrategy, Injector, NO_ERRORS_SCHEMA, Component } from '@angular/core';
import { MenuService } from './menu.service'; import { MenuService } from './menu.service';
import { MenuComponent } from './menu.component'; import { MenuComponent } from './menu.component';
import { MenuServiceStub } from '../testing/menu-service.stub'; import { of as observableOf, BehaviorSubject } from 'rxjs';
import { of as observableOf } from 'rxjs'; import { ActivatedRoute } from '@angular/router';
import { Router, ActivatedRoute } from '@angular/router';
import { RouterTestingModule } from '@angular/router/testing'; import { RouterTestingModule } from '@angular/router/testing';
import { MenuSection } from './menu-section.model'; import { MenuSection } from './menu-section.model';
import { MenuID } from './menu-id.model'; import { MenuID } from './menu-id.model';
@@ -15,14 +15,39 @@ import { AuthorizationDataService } from '../../core/data/feature-authorization/
import { createSuccessfulRemoteDataObject } from '../remote-data.utils'; import { createSuccessfulRemoteDataObject } from '../remote-data.utils';
import { ThemeService } from '../theme-support/theme.service'; import { ThemeService } from '../theme-support/theme.service';
import { getMockThemeService } from '../mocks/theme-service.mock'; import { getMockThemeService } from '../mocks/theme-service.mock';
import { MenuItemType } from './menu-item-type.model';
import { LinkMenuItemModel } from './menu-item/models/link.model';
import { provideMockStore, MockStore } from '@ngrx/store/testing';
import { StoreModule, Store } from '@ngrx/store';
import { authReducer } from '../../core/auth/auth.reducer';
import { storeModuleConfig, AppState } from '../../app.reducer';
import { rendersSectionForMenu } from './menu-section.decorator';
const mockMenuID = 'mock-menuID' as MenuID;
@Component({
// eslint-disable-next-line @angular-eslint/component-selector
selector: '',
template: '',
})
@rendersSectionForMenu(mockMenuID, true)
class TestExpandableMenuComponent {
}
@Component({
// eslint-disable-next-line @angular-eslint/component-selector
selector: '',
template: '',
})
@rendersSectionForMenu(mockMenuID, false)
class TestMenuComponent {
}
describe('MenuComponent', () => { describe('MenuComponent', () => {
let comp: MenuComponent; let comp: MenuComponent;
let fixture: ComponentFixture<MenuComponent>; let fixture: ComponentFixture<MenuComponent>;
let menuService: MenuService; let menuService: MenuService;
let router: any; let store: MockStore;
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' } }; const mockStatisticSection = { 'id': 'statistics_site', 'active': true, 'visible': true, 'index': 2, 'type': 'statistics', 'model': { 'type': 1, 'text': 'menu.section.statistics', 'link': 'statistics' } };
@@ -48,21 +73,55 @@ describe('MenuComponent', () => {
children: [] children: []
}; };
const initialState = {
menus: {
[mockMenuID]: {
collapsed: true,
id: mockMenuID,
previewCollapsed: true,
sectionToSubsectionIndex: {
section1: [],
},
sections: {
section1: {
id: 'section1',
active: false,
visible: true,
model: {
type: MenuItemType.LINK,
text: 'test',
link: '/test',
} as LinkMenuItemModel,
},
},
visible: true,
},
},
};
beforeEach(waitForAsync(() => { beforeEach(waitForAsync(() => {
authorizationService = jasmine.createSpyObj('authorizationService', { authorizationService = jasmine.createSpyObj('authorizationService', {
isAuthorized: observableOf(false) isAuthorized: observableOf(false)
}); });
TestBed.configureTestingModule({ void TestBed.configureTestingModule({
imports: [TranslateModule.forRoot(), NoopAnimationsModule, RouterTestingModule], imports: [
TranslateModule.forRoot(),
NoopAnimationsModule,
RouterTestingModule,
StoreModule.forRoot(authReducer, storeModuleConfig),
],
declarations: [MenuComponent], declarations: [MenuComponent],
providers: [ providers: [
Injector, Injector,
{ provide: ThemeService, useValue: getMockThemeService() }, { provide: ThemeService, useValue: getMockThemeService() },
{ provide: MenuService, useClass: MenuServiceStub }, MenuService,
provideMockStore({ initialState }),
{ provide: AuthorizationDataService, useValue: authorizationService }, { provide: AuthorizationDataService, useValue: authorizationService },
{ provide: ActivatedRoute, useValue: routeStub }, { provide: ActivatedRoute, useValue: routeStub },
TestExpandableMenuComponent,
TestMenuComponent,
], ],
schemas: [NO_ERRORS_SCHEMA] schemas: [NO_ERRORS_SCHEMA]
}).overrideComponent(MenuComponent, { }).overrideComponent(MenuComponent, {
@@ -74,13 +133,62 @@ describe('MenuComponent', () => {
fixture = TestBed.createComponent(MenuComponent); fixture = TestBed.createComponent(MenuComponent);
comp = fixture.componentInstance; // SearchPageComponent test instance comp = fixture.componentInstance; // SearchPageComponent test instance
comp.menuID = mockMenuID; comp.menuID = mockMenuID;
menuService = (comp as any).menuService; menuService = TestBed.inject(MenuService);
router = TestBed.inject(Router); store = TestBed.inject(Store) as MockStore<AppState>;
spyOn(comp as any, 'getSectionDataInjector').and.returnValue(MenuSection); spyOn(comp as any, 'getSectionDataInjector').and.returnValue(MenuSection);
spyOn(comp as any, 'getSectionComponent').and.returnValue(observableOf({}));
fixture.detectChanges(); fixture.detectChanges();
}); });
describe('ngOnInit', () => {
it('should trigger the section observable again when a new sub section has been added', () => {
spyOn(comp.sectionMap$, 'next').and.callThrough();
const hasSubSections = new BehaviorSubject(false);
spyOn(menuService, 'hasSubSections').and.returnValue(hasSubSections.asObservable());
spyOn(store, 'dispatch').and.callThrough();
store.setState({
menus: {
[mockMenuID]: {
collapsed: true,
id: mockMenuID,
previewCollapsed: true,
sectionToSubsectionIndex: {
section1: ['test'],
},
sections: {
section1: {
id: 'section1',
active: false,
visible: true,
model: {
type: MenuItemType.LINK,
text: 'test',
link: '/test',
} as LinkMenuItemModel,
},
test: {
id: 'test',
parentID: 'section1',
active: false,
visible: true,
model: {
type: MenuItemType.LINK,
text: 'test',
link: '/test',
} as LinkMenuItemModel,
}
},
visible: true,
},
},
});
expect(menuService.hasSubSections).toHaveBeenCalled();
hasSubSections.next(true);
expect(comp.sectionMap$.next).toHaveBeenCalled();
});
});
describe('toggle', () => { describe('toggle', () => {
beforeEach(() => { beforeEach(() => {
spyOn(menuService, 'toggleMenu'); spyOn(menuService, 'toggleMenu');

View File

@@ -6,7 +6,6 @@ import { GenericConstructor } from '../../core/shared/generic-constructor';
import { hasValue, isNotEmptyOperator } from '../empty.util'; import { hasValue, isNotEmptyOperator } from '../empty.util';
import { MenuSectionComponent } from './menu-section/menu-section.component'; import { MenuSectionComponent } from './menu-section/menu-section.component';
import { getComponentForMenu } from './menu-section.decorator'; import { getComponentForMenu } from './menu-section.decorator';
import { compareArraysUsingIds } from '../../item-page/simple/item-types/shared/item-relationships-utils';
import { MenuSection } from './menu-section.model'; import { MenuSection } from './menu-section.model';
import { MenuID } from './menu-id.model'; import { MenuID } from './menu-id.model';
import { ActivatedRoute } from '@angular/router'; import { ActivatedRoute } from '@angular/router';
@@ -86,7 +85,7 @@ export class MenuComponent implements OnInit, OnDestroy {
this.menuCollapsed = this.menuService.isMenuCollapsed(this.menuID); this.menuCollapsed = this.menuService.isMenuCollapsed(this.menuID);
this.menuPreviewCollapsed = this.menuService.isMenuPreviewCollapsed(this.menuID); this.menuPreviewCollapsed = this.menuService.isMenuPreviewCollapsed(this.menuID);
this.menuVisible = this.menuService.isMenuVisible(this.menuID); this.menuVisible = this.menuService.isMenuVisible(this.menuID);
this.sections = this.menuService.getMenuTopSections(this.menuID).pipe(distinctUntilChanged(compareArraysUsingIds())); this.sections = this.menuService.getMenuTopSections(this.menuID);
this.subs.push( this.subs.push(
this.sections.pipe( this.sections.pipe(
@@ -103,7 +102,7 @@ export class MenuComponent implements OnInit, OnDestroy {
switchMap((section: MenuSection) => this.getSectionComponent(section).pipe( switchMap((section: MenuSection) => this.getSectionComponent(section).pipe(
map((component: GenericConstructor<MenuSectionComponent>) => ({ section, component })) map((component: GenericConstructor<MenuSectionComponent>) => ({ section, component }))
)), )),
distinctUntilChanged((x, y) => x.section.id === y.section.id) distinctUntilChanged((x, y) => x.section.id === y.section.id && x.component.prototype === y.component.prototype),
).subscribe(({ section, component }) => { ).subscribe(({ section, component }) => {
const nextMap = this.sectionMap$.getValue(); const nextMap = this.sectionMap$.getValue();
nextMap.set(section.id, { nextMap.set(section.id, {

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 { distinctUntilChanged, filter, map, switchMap, take } from 'rxjs/operators';
import { import {
ActivateMenuSectionAction, ActivateMenuSectionAction,
AddMenuSectionAction, AddMenuSectionAction,
@@ -23,6 +23,7 @@ import { MenuSections } from './menu-sections.model';
import { MenuSection } from './menu-section.model'; import { MenuSection } from './menu-section.model';
import { MenuID } from './menu-id.model'; import { MenuID } from './menu-id.model';
import { ActivatedRoute, NavigationEnd, Router } from '@angular/router'; import { ActivatedRoute, NavigationEnd, Router } from '@angular/router';
import { compareArraysUsingIds } from '../../item-page/simple/item-types/shared/item-relationships-utils';
export function menuKeySelector<T>(key: string, selector): MemoizedSelector<MenuState, T> { export function menuKeySelector<T>(key: string, selector): MemoizedSelector<MenuState, T> {
return createSelector(selector, (state) => { return createSelector(selector, (state) => {
@@ -81,8 +82,10 @@ export class MenuService {
return this.store.pipe( return this.store.pipe(
select(menuByIDSelector(menuID)), select(menuByIDSelector(menuID)),
select(menuSectionStateSelector), select(menuSectionStateSelector),
map((sections: MenuSections) => { map((sections: MenuSections) => Object.values(sections)),
return Object.values(sections) distinctUntilChanged(compareArraysUsingIds()),
map((sections: MenuSection[]) => {
return sections
.filter((section: MenuSection) => hasNoValue(section.parentID)) .filter((section: MenuSection) => hasNoValue(section.parentID))
.filter((section: MenuSection) => !mustBeVisible || section.visible); .filter((section: MenuSection) => !mustBeVisible || section.visible);
} }