From a46a30e7c71b2f611ac34f3e759fab1ef31591ae Mon Sep 17 00:00:00 2001 From: Clemente Raposo Date: Fri, 26 Jun 2020 16:52:52 +0100 Subject: [PATCH] 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 --- config/services.yaml | 1 + .../legacy/exposed_system_configs.yaml | 1 + .../themes/navigation_tab_limits.yaml | 7 ++ .../app/src/components/navbar/navbar-model.ts | 3 +- .../src/components/navbar/navbar.abstract.ts | 16 +++-- .../src/components/navbar/navbar.component.ts | 41 +++++++++--- .../screen-size-observer.service.ts | 64 +++++++++++++++++++ .../src/store/navigation/navigation.store.ts | 1 + core/legacy/SystemConfigHandler.php | 5 +- .../core/legacy/SystemConfigHandlerTest.php | 12 +++- 10 files changed, 135 insertions(+), 16 deletions(-) create mode 100644 config/services/themes/navigation_tab_limits.yaml create mode 100644 core/app/src/services/ui/screen-size-observer/screen-size-observer.service.ts diff --git a/config/services.yaml b/config/services.yaml index d4c65d209..109ed7d0b 100644 --- a/config/services.yaml +++ b/config/services.yaml @@ -34,6 +34,7 @@ services: $frontendExcludedModules: '%legacy.frontend_excluded_modules%' $datetimeFormatMap: '%legacy.datetime_format_map%' $cacheResetActions: '%legacy.cache_reset_actions%' + $navigationTabLimits: '%themes.navigation_tab_limits%' _instanceof: App\Service\ProcessHandlerInterface: tags: ['app.process.handler'] diff --git a/config/services/legacy/exposed_system_configs.yaml b/config/services/legacy/exposed_system_configs.yaml index f88784382..a3e693a42 100644 --- a/config/services/legacy/exposed_system_configs.yaml +++ b/config/services/legacy/exposed_system_configs.yaml @@ -17,3 +17,4 @@ parameters: currency: true list_max_entries_per_page: true cache_reset_actions: true + navigation_tab_limits: true diff --git a/config/services/themes/navigation_tab_limits.yaml b/config/services/themes/navigation_tab_limits.yaml new file mode 100644 index 000000000..17dfe0ac2 --- /dev/null +++ b/config/services/themes/navigation_tab_limits.yaml @@ -0,0 +1,7 @@ +parameters: + themes.navigation_tab_limits: + XSmall: 4 + Small: 4 + Medium: 6 + Large: 10 + XLarge: 12 \ No newline at end of file diff --git a/core/app/src/components/navbar/navbar-model.ts b/core/app/src/components/navbar/navbar-model.ts index f5e15395c..fcc63c39e 100644 --- a/core/app/src/components/navbar/navbar-model.ts +++ b/core/app/src/components/navbar/navbar-model.ts @@ -25,7 +25,8 @@ export interface NavbarModel { languages: LanguageStrings, userPreferences: UserPreferenceMap, currentUser: CurrentUserModel, - appState: AppState + appState: AppState, + itemThreshold: number ): void; buildGroupTabMenu( diff --git a/core/app/src/components/navbar/navbar.abstract.ts b/core/app/src/components/navbar/navbar.abstract.ts index 01223dd1c..21906e8e2 100644 --- a/core/app/src/components/navbar/navbar.abstract.ts +++ b/core/app/src/components/navbar/navbar.abstract.ts @@ -129,13 +129,15 @@ export class NavbarAbstract implements NavbarModel { * @param {object} userPreferences info * @param {object} currentUser info * @param {object} appState info + * @param {number} maxTabs to display */ public build( navigation: Navigation, language: LanguageStrings, userPreferences: UserPreferenceMap, currentUser: CurrentUserModel, - appState: AppState + appState: AppState, + maxTabs: number ): void { this.resetMenu(); @@ -150,12 +152,12 @@ export class NavbarAbstract implements NavbarModel { const sort = userPreferences.sort_modules_by_name.toString() === 'on'; if (navigationParadigm === 'm') { - this.buildModuleNavigation(navigation, language, appState, sort); + this.buildModuleNavigation(navigation, language, appState, maxTabs, sort); return; } if (navigationParadigm === 'gm') { - this.buildGroupedNavigation(navigation, language, appState, sort); + this.buildGroupedNavigation(navigation, language, appState, maxTabs, sort); return; } } @@ -224,12 +226,14 @@ export class NavbarAbstract implements NavbarModel { * @param {object} navigation info * @param {object} languages map * @param {object} appState info + * @param {number} maxTabs to use * @param {boolean} sort flag */ protected buildModuleNavigation( navigation: Navigation, languages: LanguageStrings, appState: AppState, + maxTabs: number, sort: boolean ): void { @@ -237,7 +241,7 @@ export class NavbarAbstract implements NavbarModel { 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); } @@ -247,12 +251,14 @@ export class NavbarAbstract implements NavbarModel { * @param {object} navigation info * @param {object} languages map * @param {object} appState info + * @param {number} maxTabs to use * @param {boolean} sort flag */ protected buildGroupedNavigation( navigation: Navigation, languages: LanguageStrings, appState: AppState, + maxTabs: number, sort: boolean ): void { @@ -260,7 +266,7 @@ export class NavbarAbstract implements NavbarModel { 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); } diff --git a/core/app/src/components/navbar/navbar.component.ts b/core/app/src/components/navbar/navbar.component.ts index d0eb97342..2e8c2600c 100644 --- a/core/app/src/components/navbar/navbar.component.ts +++ b/core/app/src/components/navbar/navbar.component.ts @@ -6,7 +6,7 @@ import {ApiService} from '@services/api/api.service'; import {NavbarModel} from './navbar-model'; import {NavbarAbstract} from './navbar.abstract'; 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 {SystemConfigStore} from '@store/system-config/system-config.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 {ActionNameMapper} from '@services/navigation/action-name-mapper/action-name-mapper.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({ selector: 'scrm-navbar-ui', @@ -35,10 +36,12 @@ export class NavbarUiComponent implements OnInit, OnDestroy { backLink = false; mainNavLink = true; submenu: any = []; - moduleNameMapper = new ModuleNameMapper(this.systemConfigStore) - actionNameMapper = new ActionNameMapper(this.systemConfigStore) - routeConverter = new RouteConverter(this.moduleNameMapper, this.actionNameMapper) + moduleNameMapper = new ModuleNameMapper(this.systemConfigStore); + actionNameMapper = new ActionNameMapper(this.systemConfigStore); + routeConverter = new RouteConverter(this.moduleNameMapper, this.actionNameMapper); navbar: NavbarModel = new NavbarAbstract(this.routeConverter, this.moduleNavigation); + maxTabs = 8; + screen: ScreenSize = ScreenSize.Medium; languages$: Observable = this.languageStore.vm$; userPreferences$: Observable = this.userPreferenceStore.userPreferences$; @@ -51,16 +54,24 @@ export class NavbarUiComponent implements OnInit, OnDestroy { this.languages$, this.userPreferences$, this.currentUser$, - this.appState$ + this.appState$, + this.screenSize.screenSize$ ]).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( navigation, languages, userPreferences, currentUser, - appState + appState, + this.maxTabs ); return { @@ -76,7 +87,8 @@ export class NavbarUiComponent implements OnInit, OnDestroy { protected systemConfigStore: SystemConfigStore, protected appState: AppStateStore, private authService: AuthService, - protected moduleNavigation: ModuleNavigation + protected moduleNavigation: ModuleNavigation, + protected screenSize: ScreenSizeObserverService ) { const navbar = new NavbarAbstract(this.routeConverter, this.moduleNavigation); this.setNavbar(navbar); @@ -171,4 +183,17 @@ export class NavbarUiComponent implements OnInit, OnDestroy { protected isLoaded(): boolean { 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; + } + } } diff --git a/core/app/src/services/ui/screen-size-observer/screen-size-observer.service.ts b/core/app/src/services/ui/screen-size-observer/screen-size-observer.service.ts new file mode 100644 index 000000000..c4af90d0c --- /dev/null +++ b/core/app/src/services/ui/screen-size-observer/screen-size-observer.service.ts @@ -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; + + 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; + } + })) + ); + } +} diff --git a/core/app/src/store/navigation/navigation.store.ts b/core/app/src/store/navigation/navigation.store.ts index 40355fe93..ad9e2abfb 100644 --- a/core/app/src/store/navigation/navigation.store.ts +++ b/core/app/src/store/navigation/navigation.store.ts @@ -46,6 +46,7 @@ export interface ModuleAction { url: string; params?: string; icon: string; + module?: string; } const initialState: Navigation = { diff --git a/core/legacy/SystemConfigHandler.php b/core/legacy/SystemConfigHandler.php index 4bdaaf6d9..dbc5d304e 100644 --- a/core/legacy/SystemConfigHandler.php +++ b/core/legacy/SystemConfigHandler.php @@ -48,6 +48,7 @@ class SystemConfigHandler extends LegacyHandler implements SystemConfigProviderI * @param SystemConfigMappers $mappers * @param array $systemConfigKeyMap * @param array $cacheResetActions + * @param array $navigationTabLimits */ public function __construct( string $projectDir, @@ -61,7 +62,8 @@ class SystemConfigHandler extends LegacyHandler implements SystemConfigProviderI ClassicViewRoutingExclusionsHandler $exclusionsManager, SystemConfigMappers $mappers, array $systemConfigKeyMap, - array $cacheResetActions + array $cacheResetActions, + array $navigationTabLimits ) { parent::__construct($projectDir, $legacyDir, $legacySessionName, $defaultSessionName, $legacyScopeState); $this->exposedSystemConfigs = $exposedSystemConfigs; @@ -70,6 +72,7 @@ class SystemConfigHandler extends LegacyHandler implements SystemConfigProviderI $this->injectedSystemConfigs['action_name_map'] = $actionNameMapper->getMap(); $this->injectedSystemConfigs['classicview_routing_exclusions'] = $exclusionsManager->get(); $this->injectedSystemConfigs['cache_reset_actions'] = $cacheResetActions; + $this->injectedSystemConfigs['navigation_tab_limits'] = $navigationTabLimits; $this->mappers = $mappers; $this->systemConfigKeyMap = $systemConfigKeyMap; } diff --git a/tests/unit/core/legacy/SystemConfigHandlerTest.php b/tests/unit/core/legacy/SystemConfigHandlerTest.php index ad350dfb4..166e0eee1 100644 --- a/tests/unit/core/legacy/SystemConfigHandlerTest.php +++ b/tests/unit/core/legacy/SystemConfigHandlerTest.php @@ -137,6 +137,15 @@ class SystemConfigHandlerTest extends Unit ] ]; + $navigationTabLimits = [ + 'XSmall' => 4, + 'Small' => 4, + 'Medium' => 6, + 'Large' => 10, + 'XLarge' => 12 + ]; + + $this->handler = new SystemConfigHandler( $projectDir, $legacyDir, @@ -149,7 +158,8 @@ class SystemConfigHandlerTest extends Unit $classicViewExclusionHandler, $mappers, $systemConfigKeyMap, - $cacheResetActions + $cacheResetActions, + $navigationTabLimits ); }