From 1f9cd29c0e7b2b7d14f0d1c443e87d1c7cb6979f Mon Sep 17 00:00:00 2001 From: maxceem Date: Sun, 29 Nov 2020 15:43:11 +0200 Subject: [PATCH] feat: disable/enable sidebar --- .nvmrc | 1 + README.md | 20 +++++--- src/App.jsx | 55 +++++++++++++--------- src/actions/menu.js | 26 ++++++++++ src/components/AllAppsMenu/index.jsx | 2 +- src/components/MainMenu/styles.css | 35 +++++++++++++- src/components/NavBar/index.jsx | 2 +- src/constants/index.js | 2 + src/global.css | 14 +----- src/hooks/useMatchSomeRoute.js | 27 +++++++++++ src/reducers/menu.js | 55 +++++++++++++++++++--- src/root.component.js | 12 +++-- src/topcoder-micro-frontends-navbar-app.js | 12 ++++- src/utils/exports.js | 8 +++- 14 files changed, 214 insertions(+), 57 deletions(-) create mode 100644 .nvmrc create mode 100644 src/hooks/useMatchSomeRoute.js diff --git a/.nvmrc b/.nvmrc new file mode 100644 index 0000000..c2f6421 --- /dev/null +++ b/.nvmrc @@ -0,0 +1 @@ +10.22.1 diff --git a/README.md b/README.md index 413234e..3ec4783 100644 --- a/README.md +++ b/README.md @@ -64,6 +64,8 @@ This app exports functions to be imported by other microapps. - `setAppMenu` - sets sidebar menu for the app by app's `path` - `getAuthUserTokens` - returns a promise which resolves to object with user tokens `{ tokenV3, tokenV2 }` - `getAuthUserProfile` - returns a promise which resolves to the user profile object +- `disableSidebarForRoute` - disable (remove) sidebar for some route +- `enableSidebarForRoute` - enable sidebar for the route, which was previously disabled #### How to export @@ -89,13 +91,15 @@ For example see https://github.com/topcoder-platform/micro-frontends-react-app 2. As `importmaps` only work in browser and don't work in unit test, we have to mock this module in unit tests. For example by creating a file `src/__mocks__/@topcoder/micro-frontends-navbar-app.js` with the content like: ```js - module.exports = { + module.exports = { login: () => {}, logout: () => {}, setAppMenu: () => {}, getAuthUserTokens: () => new Promise(() => {}), getAuthUserProfile: () => new Promise(() => {}), - }; + disableSidebarForRoute: () => {}, + enableSidebarForRoute: () => {}, + }; ``` ##### How to import in Angular app @@ -112,11 +116,13 @@ For example see https://github.com/topcoder-platform/micro-frontends-angular-app 2. Add type definition in `src/typings.d.ts`: ```js declare module '@topcoder/micro-frontends-navbar-app' { - export const login: any; - export const logout: any; - export const setAppMenu: any; - export const getAuthUserTokens: any; - export const getAuthUserProfile: any; + export const login: any; + export const logout: any; + export const setAppMenu: any; + export const getAuthUserTokens: any; + export const getAuthUserProfile: any; + export const disableSidebarForRoute: any; + export const enableSidebarForRoute: any; } ``` diff --git a/src/App.jsx b/src/App.jsx index 91f41fb..58aa4dc 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -1,21 +1,23 @@ /** * Main App component */ -import React, { useState, useCallback, useMemo } from "react"; +import React, { useState, useCallback, useMemo, useEffect } from "react"; import _ from "lodash"; import MainMenu from "./components/MainMenu"; import NavBar from "./components/NavBar"; -import { Router, createHistory, LocationProvider } from "@reach/router"; +import { Router } from "@reach/router"; import { useSelector } from "react-redux"; - -// History for location provider -let history = createHistory(window); +import useMatchSomeRoute from "./hooks/useMatchSomeRoute"; const App = () => { // all menu options - const menu = useSelector((state) => state.menu); + const menu = useSelector((state) => state.menu.categories); // flat list of all apps (only updated when menu updated in the Redux store) const apps = useMemo(() => _.flatMap(menu, "apps"), [menu]); + // list of routes where we have to disabled sidebar + const disabledRoutes = useSelector((state) => state.menu.disabledRoutes); + // `true` is sidebar has to be disabled for the current route + const isSideBarDisabled = useMatchSomeRoute(disabledRoutes); // Left sidebar collapse state const [sidebarCollapsed, setSidebarCollapsed] = useState(false); // Toggle left sidebar callback @@ -23,23 +25,34 @@ const App = () => { setSidebarCollapsed(!sidebarCollapsed); }, [sidebarCollapsed, setSidebarCollapsed]); + // set/remove class for the whole page, to know if sidebar is present or no + useEffect(() => { + if (isSideBarDisabled) { + document.body.classList.add("no-sidebar"); + } else { + document.body.classList.remove("no-sidebar"); + } + }, [isSideBarDisabled]); + return ( - + <> -
- - {apps.map((app) => ( - - ))} - -
-
+ {!isSideBarDisabled && ( +
+ + {apps.map((app) => ( + + ))} + +
+ )} + ); }; diff --git a/src/actions/menu.js b/src/actions/menu.js index e1aab9e..835c9b7 100644 --- a/src/actions/menu.js +++ b/src/actions/menu.js @@ -13,4 +13,30 @@ export default { type: ACTIONS.MENU.SET_APP_MENU, payload: { path, menuOptions }, }), + + /** + * Disable sidebar for route. + * + * See how we can define route here https://reach.tech/router/api/Match. + * + * @param {String} route route path + * + * @returns {{ type: String, payload: any }} action object + */ + disableSidebarForRoute: (route) => ({ + type: ACTIONS.MENU.DISABLE_SIDEBAR_FOR_ROUTE, + payload: route, + }), + + /** + * Enable sidebar for route. + * + * @param {String} route route path + * + * @returns {{ type: String, payload: any }} action object + */ + enableSidebarForRoute: (route) => ({ + type: ACTIONS.MENU.ENABLE_SIDEBAR_FOR_ROUTE, + payload: route, + }), }; diff --git a/src/components/AllAppsMenu/index.jsx b/src/components/AllAppsMenu/index.jsx index 5765b0e..171b5f9 100644 --- a/src/components/AllAppsMenu/index.jsx +++ b/src/components/AllAppsMenu/index.jsx @@ -12,7 +12,7 @@ import "./styles.css"; import { useSelector } from "react-redux"; const AllAppsMenu = () => { - const menu = useSelector((state) => state.menu); + const menu = useSelector((state) => state.menu.categories); const [isOpenMenu, setIsOpenMenu] = useState(false); const closeMenu = useCallback(() => { diff --git a/src/components/MainMenu/styles.css b/src/components/MainMenu/styles.css index b8fa15b..083dd85 100644 --- a/src/components/MainMenu/styles.css +++ b/src/components/MainMenu/styles.css @@ -1,6 +1,17 @@ +/* this style controls the sidebar, + but it also has to set the margin for the main content of the main frame */ +#single-spa-main { + margin-left: 0; +} + +.main-menu-wrapper { + background-color: #fff; +} + .main-menu-header { margin: 22px 0 20px 21px; } + .menu-toggle-icon { width: 10px; height: 10px; @@ -8,7 +19,6 @@ cursor: pointer; } - .main-menu-mobile { background-color: #fff; left: 0; @@ -32,8 +42,9 @@ } .main-menu { - width: 270px; + width: var(--sideBarWidth); } + .main-menu-collapsed { width: auto; } @@ -95,3 +106,23 @@ transform: rotate(180deg); } } + +@media (min-width: 1024px) { + /* this style controls the sidebar, + but it also has to set the margin for the main content of the main frame */ + #single-spa-main { + margin-left: var(--sideBarWidth); + } + + /* if sidebar is disabled, then remove margin for the main content */ + body.no-sidebar #single-spa-main { + margin-left: 0; + } + + .main-menu-wrapper { + left: 0; + height: calc(100vh - var(--navbarHeight)); + position: absolute; + top: var(--navbarHeight); + } +} diff --git a/src/components/NavBar/index.jsx b/src/components/NavBar/index.jsx index 0a37a8e..a2276ef 100644 --- a/src/components/NavBar/index.jsx +++ b/src/components/NavBar/index.jsx @@ -17,7 +17,7 @@ import NotificationsMenu from "../NotificationsMenu"; const NavBar = () => { // all menu options - const menu = useSelector((state) => state.menu); + const menu = useSelector((state) => state.menu.categories); // flat list of all apps const apps = useMemo(() => _.flatMap(menu, "apps"), [menu]); // Active app diff --git a/src/constants/index.js b/src/constants/index.js index bfafa76..7df810f 100644 --- a/src/constants/index.js +++ b/src/constants/index.js @@ -13,5 +13,7 @@ export const ACTIONS = { }, MENU: { SET_APP_MENU: "SET_APP_MENU", + DISABLE_SIDEBAR_FOR_ROUTE: "DISABLE_SIDEBAR_FOR_ROUTE", + ENABLE_SIDEBAR_FOR_ROUTE: "ENABLE_SIDEBAR_FOR_ROUTE", }, }; diff --git a/src/global.css b/src/global.css index 3c2634a..6bc313a 100644 --- a/src/global.css +++ b/src/global.css @@ -1,5 +1,6 @@ :root { --navbarHeight: 60px; + --sideBarWidth: 270px; } /* This global style is necessary to completely hide the iframe injected into @@ -8,16 +9,3 @@ border: none; display: none; } - -.main-menu-wrapper { - background-color: #fff; -} - -@media (min-width: 1024px) { - .main-menu-wrapper { - left: 0; - height: calc(100vh - var(--navbarHeight)); - position: absolute; - top: var(--navbarHeight); - } -} diff --git a/src/hooks/useMatchSomeRoute.js b/src/hooks/useMatchSomeRoute.js new file mode 100644 index 0000000..6c403bc --- /dev/null +++ b/src/hooks/useMatchSomeRoute.js @@ -0,0 +1,27 @@ +import { useLocation, matchPath } from "@reach/router"; +import _ from "lodash"; + +/** + * Check if any of the passed paths match the current route. + * + * @param {string[]} paths paths of the routes + * + * @returns {{ uri: string, path: string, params: {} }} matched route params + */ +const useMatchSomeRoute = (paths) => { + const location = useLocation(); + + return _.find(paths, (path) => { + const result = matchPath(path, location.pathname); + + return result + ? { + params: result.params, + uri: result.uri, + path, + } + : null; + }); +}; + +export default useMatchSomeRoute; diff --git a/src/reducers/menu.js b/src/reducers/menu.js index cc031db..5338139 100644 --- a/src/reducers/menu.js +++ b/src/reducers/menu.js @@ -5,9 +5,12 @@ import _ from "lodash"; import { ACTIONS, APP_CATEGORIES } from "../constants"; /** - * Default Apps Menu structure. + * Menu State Initial Structure */ -const initialState = APP_CATEGORIES; +const initialState = { + categories: APP_CATEGORIES, // Default Apps Menu structure. + disabledRoutes: [], +}; /** * Find indexes of the category and app in the menu structure by the app's path. @@ -78,7 +81,10 @@ const menuReducer = (state = initialState, action) => { switch (action.type) { case ACTIONS.MENU.SET_APP_MENU: { const { path, menuOptions } = action.payload; - const { categoryIndex, appIndex } = findIndexByPath(state, path); + const { categoryIndex, appIndex } = findIndexByPath( + state.categories, + path + ); // if we cannot find the app, just log error and don't try to update it if (categoryIndex === -1 || appIndex === -1) { @@ -86,11 +92,46 @@ const menuReducer = (state = initialState, action) => { return state; } - return updateApp(state, categoryIndex, appIndex, (app) => ({ - ...app, - menu: menuOptions, - })); + return { + ...state, + categories: updateApp( + state.categories, + categoryIndex, + appIndex, + (app) => ({ + ...app, + menu: menuOptions, + }) + ), + }; + } + + case ACTIONS.MENU.DISABLE_SIDEBAR_FOR_ROUTE: { + // if route is already disabled, don't do anything + if (state.disabledRoutes.indexOf(action.payload) > -1) { + return state; + } + + return { + ...state, + // add route to the disabled list + disabledRoutes: [...state.disabledRoutes, action.payload], + }; } + + case ACTIONS.MENU.ENABLE_SIDEBAR_FOR_ROUTE: { + // if route is not disabled, don't do anything + if (state.disabledRoutes.indexOf(action.payload) === -1) { + return state; + } + + return { + ...state, + // remove the route from the disabled list + disabledRoutes: _.without(state.disabledRoutes, action.payload), + }; + } + default: return state; } diff --git a/src/root.component.js b/src/root.component.js index 8fe4157..ff4e98f 100644 --- a/src/root.component.js +++ b/src/root.component.js @@ -5,11 +5,17 @@ import React from "react"; import App from "./App"; import { Provider } from "react-redux"; import store from "./store"; +import { createHistory, LocationProvider } from "@reach/router"; + +// History for location provider +const history = createHistory(window); export default function Root() { return ( - - - + + + + + ); } diff --git a/src/topcoder-micro-frontends-navbar-app.js b/src/topcoder-micro-frontends-navbar-app.js index 10e4be4..67ecfcf 100644 --- a/src/topcoder-micro-frontends-navbar-app.js +++ b/src/topcoder-micro-frontends-navbar-app.js @@ -12,6 +12,8 @@ import Root from "./root.component"; import "./global.css?modules=false"; import { setAppMenu, + disableSidebarForRoute, + enableSidebarForRoute, getAuthUserTokens, getAuthUserProfile, } from "./utils/exports"; @@ -31,4 +33,12 @@ const lifecycles = singleSpaReact({ export const { bootstrap, mount, unmount } = lifecycles; // list everything we want to export for other microapps here -export { login, logout, setAppMenu, getAuthUserTokens, getAuthUserProfile }; +export { + login, + logout, + setAppMenu, + getAuthUserTokens, + getAuthUserProfile, + disableSidebarForRoute, + enableSidebarForRoute, +}; diff --git a/src/utils/exports.js b/src/utils/exports.js index 0f4c3fd..ec22d26 100644 --- a/src/utils/exports.js +++ b/src/utils/exports.js @@ -9,9 +9,15 @@ import store from "../store"; import menuActions from "../actions/menu"; // bind all the actions for exporting here -export const { setAppMenu } = bindActionCreators( +export const { + setAppMenu, + disableSidebarForRoute, + enableSidebarForRoute, +} = bindActionCreators( { setAppMenu: menuActions.setAppMenu, + disableSidebarForRoute: menuActions.disableSidebarForRoute, + enableSidebarForRoute: menuActions.enableSidebarForRoute, }, store.dispatch );