mirror of
https://github.com/DSpace/dspace-angular.git
synced 2025-10-07 01:54:15 +00:00
106974: Angular SSR menu issues
This commit is contained in:
@@ -7,6 +7,7 @@ import { ServerSyncBufferEffects } from './cache/server-sync-buffer.effects';
|
||||
import { ObjectUpdatesEffects } from './data/object-updates/object-updates.effects';
|
||||
import { RouteEffects } from './services/route.effects';
|
||||
import { RouterEffects } from './router/router.effects';
|
||||
import { MenuEffects } from '../shared/menu/menu.effects';
|
||||
|
||||
export const coreEffects = [
|
||||
RequestEffects,
|
||||
@@ -18,4 +19,5 @@ export const coreEffects = [
|
||||
ObjectUpdatesEffects,
|
||||
RouteEffects,
|
||||
RouterEffects,
|
||||
MenuEffects,
|
||||
];
|
||||
|
@@ -18,6 +18,7 @@ export const MenuActionTypes = {
|
||||
EXPAND_MENU: type('dspace/menu/EXPAND_MENU'),
|
||||
SHOW_MENU: type('dspace/menu/SHOW_MENU'),
|
||||
HIDE_MENU: type('dspace/menu/HIDE_MENU'),
|
||||
REINIT_MENUS: type('dspace/menu/REINIT_MENUS'),
|
||||
COLLAPSE_MENU_PREVIEW: type('dspace/menu/COLLAPSE_MENU_PREVIEW'),
|
||||
EXPAND_MENU_PREVIEW: type('dspace/menu/EXPAND_MENU_PREVIEW'),
|
||||
ADD_SECTION: type('dspace/menu-section/ADD_SECTION'),
|
||||
@@ -115,6 +116,13 @@ export class ExpandMenuPreviewAction implements Action {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Action used to re-initialise the menus
|
||||
*/
|
||||
export class ReinitMenuAction implements Action {
|
||||
type = MenuActionTypes.REINIT_MENUS;
|
||||
}
|
||||
|
||||
// MENU SECTION ACTIONS
|
||||
/**
|
||||
* Action used to perform state changes for a section of a certain menu
|
||||
@@ -224,4 +232,5 @@ export type MenuAction =
|
||||
| DeactivateMenuSectionAction
|
||||
| ToggleActiveMenuSectionAction
|
||||
| CollapseMenuPreviewAction
|
||||
| ExpandMenuPreviewAction;
|
||||
| ExpandMenuPreviewAction
|
||||
| ReinitMenuAction;
|
||||
|
41
src/app/shared/menu/menu.effects.spec.ts
Normal file
41
src/app/shared/menu/menu.effects.spec.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import { Observable } from 'rxjs';
|
||||
import { TestBed, waitForAsync } from '@angular/core/testing';
|
||||
import { provideMockActions } from '@ngrx/effects/testing';
|
||||
import { cold, hot } from 'jasmine-marbles';
|
||||
import { MenuEffects } from './menu.effects';
|
||||
import { ReinitMenuAction } from './menu.actions';
|
||||
import { StoreAction, StoreActionTypes } from '../../store.actions';
|
||||
|
||||
describe('MenuEffects', () => {
|
||||
let menuEffects: MenuEffects;
|
||||
let actions: Observable<any>;
|
||||
beforeEach(waitForAsync(() => {
|
||||
TestBed.configureTestingModule({
|
||||
providers: [
|
||||
MenuEffects,
|
||||
provideMockActions(() => actions),
|
||||
],
|
||||
});
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
menuEffects = TestBed.inject(MenuEffects);
|
||||
});
|
||||
|
||||
describe('reinitDSOMenus', () => {
|
||||
describe('When a REHYDRATE action is triggered', () => {
|
||||
let action;
|
||||
beforeEach(() => {
|
||||
action = new StoreAction(StoreActionTypes.REHYDRATE, null);
|
||||
});
|
||||
it('should return a ReinitMenuAction', () => {
|
||||
actions = hot('--a-', {a: action});
|
||||
const expected = cold('--b-', {b: new ReinitMenuAction});
|
||||
|
||||
expect(menuEffects.reinitDSOMenus).toBeObservable(expected);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
});
|
23
src/app/shared/menu/menu.effects.ts
Normal file
23
src/app/shared/menu/menu.effects.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import { map } from 'rxjs/operators';
|
||||
import { Injectable } from '@angular/core';
|
||||
import { Actions, createEffect, ofType } from '@ngrx/effects';
|
||||
|
||||
import { StoreActionTypes } from '../../store.actions';
|
||||
import { ReinitMenuAction } from './menu.actions';
|
||||
|
||||
@Injectable()
|
||||
export class MenuEffects {
|
||||
|
||||
/**
|
||||
* When the store is rehydrated in the browser, re-initialise the menus to ensure
|
||||
* the menus with functions are loaded correctly.
|
||||
*/
|
||||
reinitDSOMenus = createEffect(() => this.actions$
|
||||
.pipe(ofType(StoreActionTypes.REHYDRATE),
|
||||
map(() => new ReinitMenuAction())
|
||||
));
|
||||
|
||||
constructor(private actions$: Actions) {
|
||||
}
|
||||
|
||||
}
|
@@ -9,7 +9,7 @@ import {
|
||||
ExpandMenuAction,
|
||||
ExpandMenuPreviewAction,
|
||||
HideMenuAction,
|
||||
HideMenuSectionAction,
|
||||
HideMenuSectionAction, ReinitMenuAction,
|
||||
RemoveMenuSectionAction,
|
||||
ShowMenuAction,
|
||||
ShowMenuSectionAction,
|
||||
@@ -317,6 +317,17 @@ describe('menusReducer', () => {
|
||||
// is mutated, and any uncaught exception will cause the test to fail
|
||||
});
|
||||
|
||||
it('should reset the menu state to the initial state when performing the REINIT_MENUS action without affecting the previous state', () => {
|
||||
dummyState[MenuID.ADMIN].visible = true;
|
||||
const state = dummyState;
|
||||
deepFreeze([state]);
|
||||
|
||||
const action = new ReinitMenuAction();
|
||||
const menusState = menusReducer(state, action);
|
||||
expect(menusState).toEqual(initialMenusState);
|
||||
|
||||
});
|
||||
|
||||
it('should set add a new section for the correct menu in response to the ADD_SECTION action', () => {
|
||||
const state = dummyState;
|
||||
const action = new AddMenuSectionAction(menuID, visibleSection1);
|
||||
|
@@ -26,36 +26,39 @@ import { MenuID } from './menu-id.model';
|
||||
* @returns {MenusState} The new, reducer MenusState
|
||||
*/
|
||||
export function menusReducer(state: MenusState = initialMenusState, action: MenuAction): MenusState {
|
||||
const menuState: MenuState = state[action.menuID];
|
||||
switch (action.type) {
|
||||
case MenuActionTypes.COLLAPSE_MENU: {
|
||||
const newMenuState = Object.assign({}, menuState, { collapsed: true });
|
||||
const newMenuState = Object.assign({}, state[action.menuID], { collapsed: true });
|
||||
return Object.assign({}, state, { [action.menuID]: newMenuState });
|
||||
}
|
||||
case MenuActionTypes.EXPAND_MENU: {
|
||||
const newMenuState = Object.assign({}, menuState, { collapsed: false });
|
||||
const newMenuState = Object.assign({}, state[action.menuID], { collapsed: false });
|
||||
return Object.assign({}, state, { [action.menuID]: newMenuState });
|
||||
}
|
||||
case MenuActionTypes.COLLAPSE_MENU_PREVIEW: {
|
||||
const newMenuState = Object.assign({}, menuState, { previewCollapsed: true });
|
||||
const newMenuState = Object.assign({}, state[action.menuID], { previewCollapsed: true });
|
||||
return Object.assign({}, state, { [action.menuID]: newMenuState });
|
||||
}
|
||||
case MenuActionTypes.EXPAND_MENU_PREVIEW: {
|
||||
const newMenuState = Object.assign({}, menuState, { previewCollapsed: false });
|
||||
const newMenuState = Object.assign({}, state[action.menuID], { previewCollapsed: false });
|
||||
return Object.assign({}, state, { [action.menuID]: newMenuState });
|
||||
}
|
||||
case MenuActionTypes.TOGGLE_MENU: {
|
||||
const menuState = state[action.menuID];
|
||||
const newMenuState = Object.assign({}, menuState, { collapsed: !menuState.collapsed });
|
||||
return Object.assign({}, state, { [action.menuID]: newMenuState });
|
||||
}
|
||||
case MenuActionTypes.SHOW_MENU: {
|
||||
const newMenuState = Object.assign({}, menuState, { visible: true });
|
||||
const newMenuState = Object.assign({}, state[action.menuID], { visible: true });
|
||||
return Object.assign({}, state, { [action.menuID]: newMenuState });
|
||||
}
|
||||
case MenuActionTypes.HIDE_MENU: {
|
||||
const newMenuState = Object.assign({}, menuState, { visible: false });
|
||||
const newMenuState = Object.assign({}, state[action.menuID], { visible: false });
|
||||
return Object.assign({}, state, { [action.menuID]: newMenuState });
|
||||
}
|
||||
case MenuActionTypes.REINIT_MENUS: {
|
||||
return Object.assign({}, initialMenusState);
|
||||
}
|
||||
case MenuActionTypes.ADD_SECTION: {
|
||||
return addSection(state, action as AddMenuSectionAction);
|
||||
}
|
||||
@@ -230,7 +233,7 @@ function toggleActiveSection(state: MenusState, action: ToggleActiveMenuSectionA
|
||||
* @param {MenuSection} section The section that will be added or replaced in the state
|
||||
* @returns {MenusState} The new reduced state
|
||||
*/
|
||||
function putSectionState(state: MenusState, action: MenuAction, section: MenuSection): MenusState {
|
||||
function putSectionState(state: MenusState, action: MenuSectionAction, section: MenuSection): MenusState {
|
||||
const menuState: MenuState = state[action.menuID];
|
||||
const newSections = Object.assign({}, menuState.sections, {
|
||||
[section.id]: section
|
||||
|
@@ -41,6 +41,7 @@ describe('MenuService', () => {
|
||||
let routeDataMenuSection: MenuSection;
|
||||
let routeDataMenuSectionResolved: MenuSection;
|
||||
let routeDataMenuChildSection: MenuSection;
|
||||
let routeDataMenuOverwrittenChildSection: MenuSection;
|
||||
let toBeRemovedMenuSection: MenuSection;
|
||||
let alreadyPresentMenuSection: MenuSection;
|
||||
let route;
|
||||
@@ -127,6 +128,17 @@ describe('MenuService', () => {
|
||||
link: ''
|
||||
} as LinkMenuItemModel
|
||||
};
|
||||
routeDataMenuOverwrittenChildSection = {
|
||||
id: 'mockChildSection',
|
||||
parentID: 'mockSection',
|
||||
active: false,
|
||||
visible: true,
|
||||
model: {
|
||||
type: MenuItemType.LINK,
|
||||
text: 'menu.section.mockChildOverwrittenSection',
|
||||
link: ''
|
||||
} as LinkMenuItemModel
|
||||
};
|
||||
toBeRemovedMenuSection = {
|
||||
id: 'toBeRemovedSection',
|
||||
active: false,
|
||||
@@ -167,7 +179,17 @@ describe('MenuService', () => {
|
||||
[MenuID.PUBLIC]: routeDataMenuChildSection
|
||||
}
|
||||
}
|
||||
},
|
||||
firstChild: {
|
||||
snapshot: {
|
||||
data: {
|
||||
menu: {
|
||||
[MenuID.PUBLIC]: routeDataMenuOverwrittenChildSection
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -541,7 +563,7 @@ describe('MenuService', () => {
|
||||
});
|
||||
|
||||
describe('buildRouteMenuSections', () => {
|
||||
it('should add and remove menu sections depending on the current route', () => {
|
||||
it('should add and remove menu sections depending on the current route and overwrite menu sections when they have the same ID with the child route version', () => {
|
||||
spyOn(service, 'addSection');
|
||||
spyOn(service, 'removeSection');
|
||||
|
||||
@@ -550,7 +572,8 @@ describe('MenuService', () => {
|
||||
service.buildRouteMenuSections(MenuID.PUBLIC);
|
||||
|
||||
expect(service.addSection).toHaveBeenCalledWith(MenuID.PUBLIC, routeDataMenuSectionResolved);
|
||||
expect(service.addSection).toHaveBeenCalledWith(MenuID.PUBLIC, routeDataMenuChildSection);
|
||||
expect(service.addSection).not.toHaveBeenCalledWith(MenuID.PUBLIC, routeDataMenuChildSection);
|
||||
expect(service.addSection).toHaveBeenCalledWith(MenuID.PUBLIC, routeDataMenuOverwrittenChildSection);
|
||||
expect(service.addSection).not.toHaveBeenCalledWith(MenuID.PUBLIC, alreadyPresentMenuSection);
|
||||
expect(service.removeSection).toHaveBeenCalledWith(MenuID.PUBLIC, toBeRemovedMenuSection.id);
|
||||
});
|
||||
|
@@ -399,7 +399,8 @@ export class MenuService {
|
||||
}
|
||||
|
||||
if (!last) {
|
||||
return [...menuSections, ...this.resolveRouteMenuSections(route.firstChild, menuID)];
|
||||
const childMenuSections = this.resolveRouteMenuSections(route.firstChild, menuID);
|
||||
return [...menuSections.filter(menu => !(childMenuSections).map(childMenu => childMenu.id).includes(menu.id)), ...childMenuSections];
|
||||
} else {
|
||||
return [...menuSections];
|
||||
}
|
||||
|
Reference in New Issue
Block a user