Skip to content
This repository was archived by the owner on Mar 13, 2025. It is now read-only.

Toggle Sidebar #5

Merged
merged 1 commit into from
Dec 3, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .nvmrc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
10.22.1
20 changes: 13 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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
Expand All @@ -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;
}
```

Expand Down
55 changes: 34 additions & 21 deletions src/App.jsx
Original file line number Diff line number Diff line change
@@ -1,45 +1,58 @@
/**
* 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
const toggleSidebar = useCallback(() => {
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 (
<LocationProvider history={history}>
<>
<NavBar />
<div className="main-menu-wrapper">
<Router>
{apps.map((app) => (
<MainMenu
sidebarCollapsed={sidebarCollapsed}
toggleSidebar={toggleSidebar}
key={app.path}
path={app.path + "/*"}
app={app}
/>
))}
</Router>
</div>
</LocationProvider>
{!isSideBarDisabled && (
<div className="main-menu-wrapper">
<Router>
{apps.map((app) => (
<MainMenu
sidebarCollapsed={sidebarCollapsed}
toggleSidebar={toggleSidebar}
key={app.path}
path={app.path + "/*"}
app={app}
/>
))}
</Router>
</div>
)}
</>
);
};

Expand Down
26 changes: 26 additions & 0 deletions src/actions/menu.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
}),
};
2 changes: 1 addition & 1 deletion src/components/AllAppsMenu/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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(() => {
Expand Down
35 changes: 33 additions & 2 deletions src/components/MainMenu/styles.css
Original file line number Diff line number Diff line change
@@ -1,14 +1,24 @@
/* 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;
margin-left: 3px;
cursor: pointer;
}


.main-menu-mobile {
background-color: #fff;
left: 0;
Expand All @@ -32,8 +42,9 @@
}

.main-menu {
width: 270px;
width: var(--sideBarWidth);
}

.main-menu-collapsed {
width: auto;
}
Expand Down Expand Up @@ -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);
}
}
2 changes: 1 addition & 1 deletion src/components/NavBar/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions src/constants/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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",
},
};
14 changes: 1 addition & 13 deletions src/global.css
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
:root {
--navbarHeight: 60px;
--sideBarWidth: 270px;
}

/* This global style is necessary to completely hide the iframe injected into
Expand All @@ -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);
}
}
27 changes: 27 additions & 0 deletions src/hooks/useMatchSomeRoute.js
Original file line number Diff line number Diff line change
@@ -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;
55 changes: 48 additions & 7 deletions src/reducers/menu.js
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -78,19 +81,57 @@ 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) {
console.error(`App is not found by path "${path}".`);
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;
}
Expand Down
12 changes: 9 additions & 3 deletions src/root.component.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
<Provider store={store}>
<App />
</Provider>
<LocationProvider history={history}>
<Provider store={store}>
<App />
</Provider>
</LocationProvider>
);
}
Loading