mirror of
https://github.com/DSpace/dspace-angular.git
synced 2025-10-07 10:04:11 +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 { ObjectUpdatesEffects } from './data/object-updates/object-updates.effects';
|
||||||
import { RouteEffects } from './services/route.effects';
|
import { RouteEffects } from './services/route.effects';
|
||||||
import { RouterEffects } from './router/router.effects';
|
import { RouterEffects } from './router/router.effects';
|
||||||
|
import { MenuEffects } from '../shared/menu/menu.effects';
|
||||||
|
|
||||||
export const coreEffects = [
|
export const coreEffects = [
|
||||||
RequestEffects,
|
RequestEffects,
|
||||||
@@ -18,4 +19,5 @@ export const coreEffects = [
|
|||||||
ObjectUpdatesEffects,
|
ObjectUpdatesEffects,
|
||||||
RouteEffects,
|
RouteEffects,
|
||||||
RouterEffects,
|
RouterEffects,
|
||||||
|
MenuEffects,
|
||||||
];
|
];
|
||||||
|
@@ -18,6 +18,7 @@ export const MenuActionTypes = {
|
|||||||
EXPAND_MENU: type('dspace/menu/EXPAND_MENU'),
|
EXPAND_MENU: type('dspace/menu/EXPAND_MENU'),
|
||||||
SHOW_MENU: type('dspace/menu/SHOW_MENU'),
|
SHOW_MENU: type('dspace/menu/SHOW_MENU'),
|
||||||
HIDE_MENU: type('dspace/menu/HIDE_MENU'),
|
HIDE_MENU: type('dspace/menu/HIDE_MENU'),
|
||||||
|
REINIT_MENUS: type('dspace/menu/REINIT_MENUS'),
|
||||||
COLLAPSE_MENU_PREVIEW: type('dspace/menu/COLLAPSE_MENU_PREVIEW'),
|
COLLAPSE_MENU_PREVIEW: type('dspace/menu/COLLAPSE_MENU_PREVIEW'),
|
||||||
EXPAND_MENU_PREVIEW: type('dspace/menu/EXPAND_MENU_PREVIEW'),
|
EXPAND_MENU_PREVIEW: type('dspace/menu/EXPAND_MENU_PREVIEW'),
|
||||||
ADD_SECTION: type('dspace/menu-section/ADD_SECTION'),
|
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
|
// MENU SECTION ACTIONS
|
||||||
/**
|
/**
|
||||||
* Action used to perform state changes for a section of a certain menu
|
* Action used to perform state changes for a section of a certain menu
|
||||||
@@ -224,4 +232,5 @@ export type MenuAction =
|
|||||||
| DeactivateMenuSectionAction
|
| DeactivateMenuSectionAction
|
||||||
| ToggleActiveMenuSectionAction
|
| ToggleActiveMenuSectionAction
|
||||||
| CollapseMenuPreviewAction
|
| 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,
|
ExpandMenuAction,
|
||||||
ExpandMenuPreviewAction,
|
ExpandMenuPreviewAction,
|
||||||
HideMenuAction,
|
HideMenuAction,
|
||||||
HideMenuSectionAction,
|
HideMenuSectionAction, ReinitMenuAction,
|
||||||
RemoveMenuSectionAction,
|
RemoveMenuSectionAction,
|
||||||
ShowMenuAction,
|
ShowMenuAction,
|
||||||
ShowMenuSectionAction,
|
ShowMenuSectionAction,
|
||||||
@@ -317,6 +317,17 @@ describe('menusReducer', () => {
|
|||||||
// is mutated, and any uncaught exception will cause the test to fail
|
// 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', () => {
|
it('should set add a new section for the correct menu in response to the ADD_SECTION action', () => {
|
||||||
const state = dummyState;
|
const state = dummyState;
|
||||||
const action = new AddMenuSectionAction(menuID, visibleSection1);
|
const action = new AddMenuSectionAction(menuID, visibleSection1);
|
||||||
|
@@ -26,36 +26,39 @@ import { MenuID } from './menu-id.model';
|
|||||||
* @returns {MenusState} The new, reducer MenusState
|
* @returns {MenusState} The new, reducer MenusState
|
||||||
*/
|
*/
|
||||||
export function menusReducer(state: MenusState = initialMenusState, action: MenuAction): MenusState {
|
export function menusReducer(state: MenusState = initialMenusState, action: MenuAction): MenusState {
|
||||||
const menuState: MenuState = state[action.menuID];
|
|
||||||
switch (action.type) {
|
switch (action.type) {
|
||||||
case MenuActionTypes.COLLAPSE_MENU: {
|
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 });
|
return Object.assign({}, state, { [action.menuID]: newMenuState });
|
||||||
}
|
}
|
||||||
case MenuActionTypes.EXPAND_MENU: {
|
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 });
|
return Object.assign({}, state, { [action.menuID]: newMenuState });
|
||||||
}
|
}
|
||||||
case MenuActionTypes.COLLAPSE_MENU_PREVIEW: {
|
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 });
|
return Object.assign({}, state, { [action.menuID]: newMenuState });
|
||||||
}
|
}
|
||||||
case MenuActionTypes.EXPAND_MENU_PREVIEW: {
|
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 });
|
return Object.assign({}, state, { [action.menuID]: newMenuState });
|
||||||
}
|
}
|
||||||
case MenuActionTypes.TOGGLE_MENU: {
|
case MenuActionTypes.TOGGLE_MENU: {
|
||||||
|
const menuState = state[action.menuID];
|
||||||
const newMenuState = Object.assign({}, menuState, { collapsed: !menuState.collapsed });
|
const newMenuState = Object.assign({}, menuState, { collapsed: !menuState.collapsed });
|
||||||
return Object.assign({}, state, { [action.menuID]: newMenuState });
|
return Object.assign({}, state, { [action.menuID]: newMenuState });
|
||||||
}
|
}
|
||||||
case MenuActionTypes.SHOW_MENU: {
|
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 });
|
return Object.assign({}, state, { [action.menuID]: newMenuState });
|
||||||
}
|
}
|
||||||
case MenuActionTypes.HIDE_MENU: {
|
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 });
|
return Object.assign({}, state, { [action.menuID]: newMenuState });
|
||||||
}
|
}
|
||||||
|
case MenuActionTypes.REINIT_MENUS: {
|
||||||
|
return Object.assign({}, initialMenusState);
|
||||||
|
}
|
||||||
case MenuActionTypes.ADD_SECTION: {
|
case MenuActionTypes.ADD_SECTION: {
|
||||||
return addSection(state, action as AddMenuSectionAction);
|
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
|
* @param {MenuSection} section The section that will be added or replaced in the state
|
||||||
* @returns {MenusState} The new reduced 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 menuState: MenuState = state[action.menuID];
|
||||||
const newSections = Object.assign({}, menuState.sections, {
|
const newSections = Object.assign({}, menuState.sections, {
|
||||||
[section.id]: section
|
[section.id]: section
|
||||||
|
@@ -41,6 +41,7 @@ describe('MenuService', () => {
|
|||||||
let routeDataMenuSection: MenuSection;
|
let routeDataMenuSection: MenuSection;
|
||||||
let routeDataMenuSectionResolved: MenuSection;
|
let routeDataMenuSectionResolved: MenuSection;
|
||||||
let routeDataMenuChildSection: MenuSection;
|
let routeDataMenuChildSection: MenuSection;
|
||||||
|
let routeDataMenuOverwrittenChildSection: MenuSection;
|
||||||
let toBeRemovedMenuSection: MenuSection;
|
let toBeRemovedMenuSection: MenuSection;
|
||||||
let alreadyPresentMenuSection: MenuSection;
|
let alreadyPresentMenuSection: MenuSection;
|
||||||
let route;
|
let route;
|
||||||
@@ -127,6 +128,17 @@ describe('MenuService', () => {
|
|||||||
link: ''
|
link: ''
|
||||||
} as LinkMenuItemModel
|
} as LinkMenuItemModel
|
||||||
};
|
};
|
||||||
|
routeDataMenuOverwrittenChildSection = {
|
||||||
|
id: 'mockChildSection',
|
||||||
|
parentID: 'mockSection',
|
||||||
|
active: false,
|
||||||
|
visible: true,
|
||||||
|
model: {
|
||||||
|
type: MenuItemType.LINK,
|
||||||
|
text: 'menu.section.mockChildOverwrittenSection',
|
||||||
|
link: ''
|
||||||
|
} as LinkMenuItemModel
|
||||||
|
};
|
||||||
toBeRemovedMenuSection = {
|
toBeRemovedMenuSection = {
|
||||||
id: 'toBeRemovedSection',
|
id: 'toBeRemovedSection',
|
||||||
active: false,
|
active: false,
|
||||||
@@ -167,9 +179,19 @@ describe('MenuService', () => {
|
|||||||
[MenuID.PUBLIC]: routeDataMenuChildSection
|
[MenuID.PUBLIC]: routeDataMenuChildSection
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
firstChild: {
|
||||||
|
snapshot: {
|
||||||
|
data: {
|
||||||
|
menu: {
|
||||||
|
[MenuID.PUBLIC]: routeDataMenuOverwrittenChildSection
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
router = {
|
router = {
|
||||||
@@ -541,7 +563,7 @@ describe('MenuService', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('buildRouteMenuSections', () => {
|
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, 'addSection');
|
||||||
spyOn(service, 'removeSection');
|
spyOn(service, 'removeSection');
|
||||||
|
|
||||||
@@ -550,7 +572,8 @@ describe('MenuService', () => {
|
|||||||
service.buildRouteMenuSections(MenuID.PUBLIC);
|
service.buildRouteMenuSections(MenuID.PUBLIC);
|
||||||
|
|
||||||
expect(service.addSection).toHaveBeenCalledWith(MenuID.PUBLIC, routeDataMenuSectionResolved);
|
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.addSection).not.toHaveBeenCalledWith(MenuID.PUBLIC, alreadyPresentMenuSection);
|
||||||
expect(service.removeSection).toHaveBeenCalledWith(MenuID.PUBLIC, toBeRemovedMenuSection.id);
|
expect(service.removeSection).toHaveBeenCalledWith(MenuID.PUBLIC, toBeRemovedMenuSection.id);
|
||||||
});
|
});
|
||||||
|
@@ -399,7 +399,8 @@ export class MenuService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!last) {
|
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 {
|
} else {
|
||||||
return [...menuSections];
|
return [...menuSections];
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user