mirror of
https://github.com/DSpace/dspace-angular.git
synced 2025-10-07 10:04:11 +00:00
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:
@@ -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');
|
||||||
|
@@ -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, {
|
||||||
|
@@ -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);
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user