Add responsive navbar layout

- Add screensize observer to check size changes
- Add navbar item limit configuration per screen size
-- exposed through system configs
- Re-adjust navbar upon screen size changes
This commit is contained in:
Clemente Raposo 2020-06-26 16:52:52 +01:00 committed by Dillon-Brown
parent a12e8af652
commit a46a30e7c7
10 changed files with 135 additions and 16 deletions

View file

@ -34,6 +34,7 @@ services:
$frontendExcludedModules: '%legacy.frontend_excluded_modules%' $frontendExcludedModules: '%legacy.frontend_excluded_modules%'
$datetimeFormatMap: '%legacy.datetime_format_map%' $datetimeFormatMap: '%legacy.datetime_format_map%'
$cacheResetActions: '%legacy.cache_reset_actions%' $cacheResetActions: '%legacy.cache_reset_actions%'
$navigationTabLimits: '%themes.navigation_tab_limits%'
_instanceof: _instanceof:
App\Service\ProcessHandlerInterface: App\Service\ProcessHandlerInterface:
tags: ['app.process.handler'] tags: ['app.process.handler']

View file

@ -17,3 +17,4 @@ parameters:
currency: true currency: true
list_max_entries_per_page: true list_max_entries_per_page: true
cache_reset_actions: true cache_reset_actions: true
navigation_tab_limits: true

View file

@ -0,0 +1,7 @@
parameters:
themes.navigation_tab_limits:
XSmall: 4
Small: 4
Medium: 6
Large: 10
XLarge: 12

View file

@ -25,7 +25,8 @@ export interface NavbarModel {
languages: LanguageStrings, languages: LanguageStrings,
userPreferences: UserPreferenceMap, userPreferences: UserPreferenceMap,
currentUser: CurrentUserModel, currentUser: CurrentUserModel,
appState: AppState appState: AppState,
itemThreshold: number
): void; ): void;
buildGroupTabMenu( buildGroupTabMenu(

View file

@ -129,13 +129,15 @@ export class NavbarAbstract implements NavbarModel {
* @param {object} userPreferences info * @param {object} userPreferences info
* @param {object} currentUser info * @param {object} currentUser info
* @param {object} appState info * @param {object} appState info
* @param {number} maxTabs to display
*/ */
public build( public build(
navigation: Navigation, navigation: Navigation,
language: LanguageStrings, language: LanguageStrings,
userPreferences: UserPreferenceMap, userPreferences: UserPreferenceMap,
currentUser: CurrentUserModel, currentUser: CurrentUserModel,
appState: AppState appState: AppState,
maxTabs: number
): void { ): void {
this.resetMenu(); this.resetMenu();
@ -150,12 +152,12 @@ export class NavbarAbstract implements NavbarModel {
const sort = userPreferences.sort_modules_by_name.toString() === 'on'; const sort = userPreferences.sort_modules_by_name.toString() === 'on';
if (navigationParadigm === 'm') { if (navigationParadigm === 'm') {
this.buildModuleNavigation(navigation, language, appState, sort); this.buildModuleNavigation(navigation, language, appState, maxTabs, sort);
return; return;
} }
if (navigationParadigm === 'gm') { if (navigationParadigm === 'gm') {
this.buildGroupedNavigation(navigation, language, appState, sort); this.buildGroupedNavigation(navigation, language, appState, maxTabs, sort);
return; return;
} }
} }
@ -224,12 +226,14 @@ export class NavbarAbstract implements NavbarModel {
* @param {object} navigation info * @param {object} navigation info
* @param {object} languages map * @param {object} languages map
* @param {object} appState info * @param {object} appState info
* @param {number} maxTabs to use
* @param {boolean} sort flag * @param {boolean} sort flag
*/ */
protected buildModuleNavigation( protected buildModuleNavigation(
navigation: Navigation, navigation: Navigation,
languages: LanguageStrings, languages: LanguageStrings,
appState: AppState, appState: AppState,
maxTabs: number,
sort: boolean sort: boolean
): void { ): void {
@ -237,7 +241,7 @@ export class NavbarAbstract implements NavbarModel {
return; return;
} }
this.buildTabMenu(navigation.tabs, navigation.modules, languages, navigation.maxTabs, appState, sort); this.buildTabMenu(navigation.tabs, navigation.modules, languages, maxTabs, appState, sort);
this.buildSelectedModule(navigation, languages, appState); this.buildSelectedModule(navigation, languages, appState);
} }
@ -247,12 +251,14 @@ export class NavbarAbstract implements NavbarModel {
* @param {object} navigation info * @param {object} navigation info
* @param {object} languages map * @param {object} languages map
* @param {object} appState info * @param {object} appState info
* @param {number} maxTabs to use
* @param {boolean} sort flag * @param {boolean} sort flag
*/ */
protected buildGroupedNavigation( protected buildGroupedNavigation(
navigation: Navigation, navigation: Navigation,
languages: LanguageStrings, languages: LanguageStrings,
appState: AppState, appState: AppState,
maxTabs: number,
sort: boolean sort: boolean
): void { ): void {
@ -260,7 +266,7 @@ export class NavbarAbstract implements NavbarModel {
return; return;
} }
this.buildGroupTabMenu(navigation.tabs, navigation.modules, languages, navigation.maxTabs, navigation.groupedTabs, sort); this.buildGroupTabMenu(navigation.tabs, navigation.modules, languages, maxTabs, navigation.groupedTabs, sort);
this.buildSelectedModule(navigation, languages, appState); this.buildSelectedModule(navigation, languages, appState);
} }

View file

@ -6,7 +6,7 @@ import {ApiService} from '@services/api/api.service';
import {NavbarModel} from './navbar-model'; import {NavbarModel} from './navbar-model';
import {NavbarAbstract} from './navbar.abstract'; import {NavbarAbstract} from './navbar.abstract';
import {Navigation, NavigationStore} from '@store/navigation/navigation.store'; import {Navigation, NavigationStore} from '@store/navigation/navigation.store';
import {UserPreferenceStore, UserPreferenceMap} from '@store/user-preference/user-preference.store'; import {UserPreferenceMap, UserPreferenceStore} from '@store/user-preference/user-preference.store';
import {AuthService} from '@services/auth/auth.service'; import {AuthService} from '@services/auth/auth.service';
import {SystemConfigStore} from '@store/system-config/system-config.store'; import {SystemConfigStore} from '@store/system-config/system-config.store';
import {AppState, AppStateStore} from '@store/app-state/app-state.store'; import {AppState, AppStateStore} from '@store/app-state/app-state.store';
@ -15,6 +15,7 @@ import {RouteConverter} from '@services/navigation/route-converter/route-convert
import {ModuleNameMapper} from '@services/navigation/module-name-mapper/module-name-mapper.service'; import {ModuleNameMapper} from '@services/navigation/module-name-mapper/module-name-mapper.service';
import {ActionNameMapper} from '@services/navigation/action-name-mapper/action-name-mapper.service'; import {ActionNameMapper} from '@services/navigation/action-name-mapper/action-name-mapper.service';
import {ModuleNavigation} from '@services/navigation/module-navigation/module-navigation.service'; import {ModuleNavigation} from '@services/navigation/module-navigation/module-navigation.service';
import {ScreenSize, ScreenSizeObserverService} from '@services/ui/screen-size-observer/screen-size-observer.service';
@Component({ @Component({
selector: 'scrm-navbar-ui', selector: 'scrm-navbar-ui',
@ -35,10 +36,12 @@ export class NavbarUiComponent implements OnInit, OnDestroy {
backLink = false; backLink = false;
mainNavLink = true; mainNavLink = true;
submenu: any = []; submenu: any = [];
moduleNameMapper = new ModuleNameMapper(this.systemConfigStore) moduleNameMapper = new ModuleNameMapper(this.systemConfigStore);
actionNameMapper = new ActionNameMapper(this.systemConfigStore) actionNameMapper = new ActionNameMapper(this.systemConfigStore);
routeConverter = new RouteConverter(this.moduleNameMapper, this.actionNameMapper) routeConverter = new RouteConverter(this.moduleNameMapper, this.actionNameMapper);
navbar: NavbarModel = new NavbarAbstract(this.routeConverter, this.moduleNavigation); navbar: NavbarModel = new NavbarAbstract(this.routeConverter, this.moduleNavigation);
maxTabs = 8;
screen: ScreenSize = ScreenSize.Medium;
languages$: Observable<LanguageStrings> = this.languageStore.vm$; languages$: Observable<LanguageStrings> = this.languageStore.vm$;
userPreferences$: Observable<UserPreferenceMap> = this.userPreferenceStore.userPreferences$; userPreferences$: Observable<UserPreferenceMap> = this.userPreferenceStore.userPreferences$;
@ -51,16 +54,24 @@ export class NavbarUiComponent implements OnInit, OnDestroy {
this.languages$, this.languages$,
this.userPreferences$, this.userPreferences$,
this.currentUser$, this.currentUser$,
this.appState$ this.appState$,
this.screenSize.screenSize$
]).pipe( ]).pipe(
map(([navigation, languages, userPreferences, currentUser, appState]) => { map(([navigation, languages, userPreferences, currentUser, appState, screenSize]) => {
if (screenSize) {
this.screen = screenSize;
}
this.calculateMaxTabs(navigation);
this.navbar.build( this.navbar.build(
navigation, navigation,
languages, languages,
userPreferences, userPreferences,
currentUser, currentUser,
appState appState,
this.maxTabs
); );
return { return {
@ -76,7 +87,8 @@ export class NavbarUiComponent implements OnInit, OnDestroy {
protected systemConfigStore: SystemConfigStore, protected systemConfigStore: SystemConfigStore,
protected appState: AppStateStore, protected appState: AppStateStore,
private authService: AuthService, private authService: AuthService,
protected moduleNavigation: ModuleNavigation protected moduleNavigation: ModuleNavigation,
protected screenSize: ScreenSizeObserverService
) { ) {
const navbar = new NavbarAbstract(this.routeConverter, this.moduleNavigation); const navbar = new NavbarAbstract(this.routeConverter, this.moduleNavigation);
this.setNavbar(navbar); this.setNavbar(navbar);
@ -171,4 +183,17 @@ export class NavbarUiComponent implements OnInit, OnDestroy {
protected isLoaded(): boolean { protected isLoaded(): boolean {
return this.loaded; return this.loaded;
} }
protected calculateMaxTabs(navigation: Navigation): void {
const sizeMap = this.systemConfigStore.getConfigValue('navigation_tab_limits');
if (this.screen && sizeMap) {
let maxTabs = sizeMap[this.screen];
if (!maxTabs || navigation.maxTabs && navigation.maxTabs < maxTabs) {
maxTabs = navigation.maxTabs;
}
this.maxTabs = maxTabs;
}
}
} }

View file

@ -0,0 +1,64 @@
import {Injectable} from '@angular/core';
import {BreakpointObserver, Breakpoints, BreakpointState} from '@angular/cdk/layout';
import {merge, Observable} from 'rxjs';
import {map} from 'rxjs/operators';
export enum ScreenSize {
XSmall = 'XSmall',
Small = 'Small',
Medium = 'Medium',
Large = 'Large',
XLarge = 'XLarge'
}
@Injectable({
providedIn: 'root'
})
export class ScreenSizeObserverService {
screenSize$: Observable<ScreenSize>;
constructor(protected breakpointObserver: BreakpointObserver) {
this.initScreenSizeObservable();
}
protected initScreenSizeObservable(): void {
this.screenSize$ = merge(
this.breakpointObserver.observe([
Breakpoints.XSmall,
]).pipe(map((result: BreakpointState) => {
if (result.matches) {
return ScreenSize.XSmall;
}
})),
this.breakpointObserver.observe([
Breakpoints.Small,
]).pipe(map((result: BreakpointState) => {
if (result.matches) {
return ScreenSize.Small;
}
})),
this.breakpointObserver.observe([
Breakpoints.Medium,
]).pipe(map((result: BreakpointState) => {
if (result.matches) {
return ScreenSize.Medium;
}
})),
this.breakpointObserver.observe([
Breakpoints.Large,
]).pipe(map((result: BreakpointState) => {
if (result.matches) {
return ScreenSize.Large;
}
})),
this.breakpointObserver.observe([
Breakpoints.XLarge,
]).pipe(map((result: BreakpointState) => {
if (result.matches) {
return ScreenSize.XLarge;
}
}))
);
}
}

View file

@ -46,6 +46,7 @@ export interface ModuleAction {
url: string; url: string;
params?: string; params?: string;
icon: string; icon: string;
module?: string;
} }
const initialState: Navigation = { const initialState: Navigation = {

View file

@ -48,6 +48,7 @@ class SystemConfigHandler extends LegacyHandler implements SystemConfigProviderI
* @param SystemConfigMappers $mappers * @param SystemConfigMappers $mappers
* @param array $systemConfigKeyMap * @param array $systemConfigKeyMap
* @param array $cacheResetActions * @param array $cacheResetActions
* @param array $navigationTabLimits
*/ */
public function __construct( public function __construct(
string $projectDir, string $projectDir,
@ -61,7 +62,8 @@ class SystemConfigHandler extends LegacyHandler implements SystemConfigProviderI
ClassicViewRoutingExclusionsHandler $exclusionsManager, ClassicViewRoutingExclusionsHandler $exclusionsManager,
SystemConfigMappers $mappers, SystemConfigMappers $mappers,
array $systemConfigKeyMap, array $systemConfigKeyMap,
array $cacheResetActions array $cacheResetActions,
array $navigationTabLimits
) { ) {
parent::__construct($projectDir, $legacyDir, $legacySessionName, $defaultSessionName, $legacyScopeState); parent::__construct($projectDir, $legacyDir, $legacySessionName, $defaultSessionName, $legacyScopeState);
$this->exposedSystemConfigs = $exposedSystemConfigs; $this->exposedSystemConfigs = $exposedSystemConfigs;
@ -70,6 +72,7 @@ class SystemConfigHandler extends LegacyHandler implements SystemConfigProviderI
$this->injectedSystemConfigs['action_name_map'] = $actionNameMapper->getMap(); $this->injectedSystemConfigs['action_name_map'] = $actionNameMapper->getMap();
$this->injectedSystemConfigs['classicview_routing_exclusions'] = $exclusionsManager->get(); $this->injectedSystemConfigs['classicview_routing_exclusions'] = $exclusionsManager->get();
$this->injectedSystemConfigs['cache_reset_actions'] = $cacheResetActions; $this->injectedSystemConfigs['cache_reset_actions'] = $cacheResetActions;
$this->injectedSystemConfigs['navigation_tab_limits'] = $navigationTabLimits;
$this->mappers = $mappers; $this->mappers = $mappers;
$this->systemConfigKeyMap = $systemConfigKeyMap; $this->systemConfigKeyMap = $systemConfigKeyMap;
} }

View file

@ -137,6 +137,15 @@ class SystemConfigHandlerTest extends Unit
] ]
]; ];
$navigationTabLimits = [
'XSmall' => 4,
'Small' => 4,
'Medium' => 6,
'Large' => 10,
'XLarge' => 12
];
$this->handler = new SystemConfigHandler( $this->handler = new SystemConfigHandler(
$projectDir, $projectDir,
$legacyDir, $legacyDir,
@ -149,7 +158,8 @@ class SystemConfigHandlerTest extends Unit
$classicViewExclusionHandler, $classicViewExclusionHandler,
$mappers, $mappers,
$systemConfigKeyMap, $systemConfigKeyMap,
$cacheResetActions $cacheResetActions,
$navigationTabLimits
); );
} }