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

Commit d3267e4

Browse files
authored
Merge pull request #4 from maxceem/feature/cross-imports
Cross Imports
2 parents 16994fb + b4a11d2 commit d3267e4

File tree

19 files changed

+559
-96
lines changed

19 files changed

+559
-96
lines changed

README.md

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,3 +56,68 @@ Make sure you have [Heroky CLI](https://devcenter.heroku.com/articles/heroku-cli
5656
- Now you have to configure frame app to use the URL provided by Heroku like `https://<APP-NAME>.herokuapp.com/topcoder-micro-frontends-navbar-app.js` to load this microapp.
5757
- NOTE: Authorization would not work because only predefined list of domain allowed by `accounts-app`.
5858

59+
### Cross microfrontend imports
60+
61+
This app exports functions to be imported by other microapps.
62+
- `login` - redirects to login page
63+
- `logout` - clears session storage and redirects to logout page
64+
- `setAppMenu` - sets sidebar menu for the app by app's `path`
65+
- `getAuthUserTokens` - returns a promise which resolves to object with user tokens `{ tokenV3, tokenV2 }`
66+
- `getAuthUserProfile` - returns a promise which resolves to the user profile object
67+
68+
#### How to export
69+
70+
- To export any function we have to `export` in file [src/topcoder-micro-frontends-navbar-app.js](src/topcoder-micro-frontends-navbar-app.js).
71+
- If we want to prepare some function for exporting, the good place to do so is inside [src/utils/exports.js](src/utils/exports.js).
72+
- We have to bind actions before exporting.
73+
- It's not recommended to export the whole Redux Store to keep only navbar responsible for updating it. It's better to create promises which would return some particular value from the store.
74+
75+
#### How to import
76+
77+
When we want to use methods exported in the navbar microapp in other apps we have to make sure that webpack would not process imports from navbar as it is handled by `importmaps`, see [Cross microfrontend imports](https://single-spa.js.org/docs/recommended-setup/#cross-microfrontend-imports).
78+
79+
##### How to import in React app
80+
81+
For example see https://github.com/topcoder-platform/micro-frontends-react-app
82+
83+
1. Add `@topcoder/micro-frontends-navbar-app` to `externals` in webpack config:
84+
```js
85+
externals: {
86+
"@topcoder/micro-frontends-navbar-app": "@topcoder/micro-frontends-navbar-app",
87+
},
88+
```
89+
90+
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:
91+
```js
92+
module.exports = {
93+
login: () => {},
94+
logout: () => {},
95+
setAppMenu: () => {},
96+
getAuthUserTokens: () => new Promise(() => {}),
97+
getAuthUserProfile: () => new Promise(() => {}),
98+
};
99+
```
100+
101+
##### How to import in Angular app
102+
103+
For example see https://github.com/topcoder-platform/micro-frontends-angular-app
104+
105+
1. Add `@topcoder/micro-frontends-navbar-app` to `externals` in webpack config:
106+
```js
107+
externals: {
108+
"@topcoder/micro-frontends-navbar-app": "@topcoder/micro-frontends-navbar-app",
109+
},
110+
```
111+
112+
2. Add type definition in `src/typings.d.ts`:
113+
```js
114+
declare module '@topcoder/micro-frontends-navbar-app' {
115+
export const login: any;
116+
export const logout: any;
117+
export const setAppMenu: any;
118+
export const getAuthUserTokens: any;
119+
export const getAuthUserProfile: any;
120+
}
121+
```
122+
123+
3. TODO: How to make e2e tests work for Angular? So far they fail with error `Module not found: Error: Can't resolve '@topcoder/micro-frontends-navbar-app'`

package-lock.json

Lines changed: 15 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
"jest-cli": "^25.2.7",
3737
"prettier": "^2.0.4",
3838
"pretty-quick": "^2.0.1",
39+
"redux-logger": "^3.0.6",
3940
"single-spa-react": "^2.14.0",
4041
"systemjs-webpack-interop": "^2.1.2",
4142
"webpack": "^4.41.2",
@@ -46,6 +47,7 @@
4647
},
4748
"dependencies": {
4849
"@reach/router": "^1.3.4",
50+
"tc-auth-lib": "topcoder-platform/tc-auth-lib#1.0.1",
4951
"browser-cookies": "^1.2.0",
5052
"classnames": "^2.2.6",
5153
"config": "^3.3.2",
@@ -56,7 +58,6 @@
5658
"react-outside-click-handler": "^1.3.0",
5759
"react-redux": "^7.2.1",
5860
"react-responsive": "^8.1.0",
59-
"redux": "^4.0.5",
60-
"tc-auth-lib": "topcoder-platform/tc-auth-lib#1.0.3"
61+
"redux": "^4.0.5"
6162
}
6263
}

src/App.jsx

Lines changed: 17 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,21 @@
11
/**
22
* Main App component
33
*/
4-
import React, {useState, useCallback} from "react";
4+
import React, { useState, useCallback, useMemo } from "react";
5+
import _ from "lodash";
56
import MainMenu from "./components/MainMenu";
67
import NavBar from "./components/NavBar";
7-
import { Router } from "@reach/router";
8-
import { APPS } from "./constants";
9-
import {
10-
createHistory,
11-
LocationProvider
12-
} from "@reach/router"
8+
import { Router, createHistory, LocationProvider } from "@reach/router";
9+
import { useSelector } from "react-redux";
1310

1411
// History for location provider
15-
let history = createHistory(window)
12+
let history = createHistory(window);
1613

1714
const App = () => {
15+
// all menu options
16+
const menu = useSelector((state) => state.menu);
17+
// flat list of all apps (only updated when menu updated in the Redux store)
18+
const apps = useMemo(() => _.flatMap(menu, "apps"), [menu]);
1819
// Left sidebar collapse state
1920
const [sidebarCollapsed, setSidebarCollapsed] = useState(false);
2021
// Toggle left sidebar callback
@@ -27,8 +28,14 @@ const App = () => {
2728
<NavBar />
2829
<div className="main-menu-wrapper">
2930
<Router>
30-
{APPS.map((app) => (
31-
<MainMenu sidebarCollapsed={sidebarCollapsed} toggleSidebar={toggleSidebar} key={app.path} path={app.path + "/*"} app={app} />
31+
{apps.map((app) => (
32+
<MainMenu
33+
sidebarCollapsed={sidebarCollapsed}
34+
toggleSidebar={toggleSidebar}
35+
key={app.path}
36+
path={app.path + "/*"}
37+
app={app}
38+
/>
3239
))}
3340
</Router>
3441
</div>

src/actions/index.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import auth from "./auth";
2+
import menu from "./menu";
23

34
export default {
45
auth,
6+
menu,
57
};

src/actions/menu.js

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import { ACTIONS } from "../constants";
2+
3+
export default {
4+
/**
5+
* Set menu options for the app in the side-bar.
6+
*
7+
* @param {String} path app path
8+
* @param {Array} menuOptions menu options
9+
*
10+
* @returns {{ type: String, payload: any }} action object
11+
*/
12+
setAppMenu: (path, menuOptions) => ({
13+
type: ACTIONS.MENU.SET_APP_MENU,
14+
payload: { path, menuOptions },
15+
}),
16+
};

src/components/AllAppsMenu/index.jsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,11 @@ import { Link } from "@reach/router";
88
import cn from "classnames";
99
import OutsideClickHandler from "react-outside-click-handler";
1010
import AllAppsMenuIcon from "../../assets/images/all-apps-menu.svg";
11-
import { APP_CATEGORIES } from "../../constants";
1211
import "./styles.css";
12+
import { useSelector } from "react-redux";
1313

1414
const AllAppsMenu = () => {
15+
const menu = useSelector((state) => state.menu);
1516
const [isOpenMenu, setIsOpenMenu] = useState(false);
1617

1718
const closeMenu = useCallback(() => {
@@ -41,7 +42,7 @@ const AllAppsMenu = () => {
4142
<div className="all-apps-menu-popover-content">
4243
<div className="all-apps-menu-list-title">SWITCH TOOLS</div>
4344
<ul className="all-apps-menu-list">
44-
{APP_CATEGORIES.map((appCategory) => (
45+
{menu.map((appCategory) => (
4546
<Fragment>
4647
<div className="switch-category-title">
4748
<div className="menu-divider"></div>

src/components/NavBar/index.jsx

Lines changed: 16 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -3,34 +3,34 @@
33
*
44
* Shows global top navigation bar with all apps menu, logo and user menu.
55
*/
6-
import React, {useState, useCallback, Fragment} from "react";
6+
import React, {useState, useCallback, Fragment, useEffect, useMemo} from "react";
7+
import _ from 'lodash';
78
import UserMenu from "../UserMenu";
89
import AllAppsMenu from "../AllAppsMenu";
910
import { useSelector } from "react-redux";
1011
import { Link, useLocation } from "@reach/router";
1112
import TCLogo from "../../assets/images/tc-logo.svg";
12-
import config from "../../../config";
13+
import { getLoginUrl } from "../../utils";
1314
import "./styles.css";
1415
import { useMediaQuery } from "react-responsive";
1516
import NotificationsMenu from "../NotificationsMenu";
16-
import { useEffect } from "react";
17-
import { APPS } from '../../constants';
1817

1918
const NavBar = () => {
19+
// all menu options
20+
const menu = useSelector((state) => state.menu);
21+
// flat list of all apps
22+
const apps = useMemo(() => _.flatMap(menu, "apps"), [menu]);
2023
// Active app
2124
const [activeApp, setActiveApp] = useState(null);
2225
const auth = useSelector((state) => state.auth);
23-
const loginUrl = `${config.URL.AUTH}/member?retUrl=${encodeURIComponent(
24-
window.location.href.match(/[^?]*/)[0]
25-
)}`;
2626
const isMobile = useMediaQuery({
2727
query: "(max-width: 1023px)",
2828
});
29-
29+
3030
const routerLocation = useLocation();
3131
// Check app title with route activated
3232
useEffect(() => {
33-
const activeApp = APPS.find(f => routerLocation.pathname.indexOf(f.path) !== -1);
33+
const activeApp = apps.find(f => routerLocation.pathname.indexOf(f.path) !== -1);
3434
setActiveApp(activeApp);
3535
}, [routerLocation])
3636

@@ -42,11 +42,11 @@ const NavBar = () => {
4242
return (
4343
<div className="navbar">
4444
<div className="navbar-left">
45-
{isMobile ?
45+
{isMobile ?
4646
(
4747
<AllAppsMenu/>
48-
)
49-
:
48+
)
49+
:
5050
(
5151
<Fragment>
5252
<Link to="/">
@@ -57,7 +57,7 @@ const NavBar = () => {
5757
</Fragment>
5858
)
5959
}
60-
60+
6161
</div>
6262

6363
<div className="navbar-center">
@@ -80,7 +80,7 @@ const NavBar = () => {
8080
</Fragment>
8181
)
8282
) : (
83-
<a href={loginUrl} className="navbar-login">
83+
<a href={getLoginUrl()} className="navbar-login">
8484
Login
8585
</a>
8686
))}
@@ -98,13 +98,13 @@ const NavBar = () => {
9898
</Fragment>
9999
)
100100
) : (
101-
<a href={loginUrl} className="navbar-login">
101+
<a href={getLoginUrl()} className="navbar-login">
102102
Login
103103
</a>
104104
))}
105105
</Fragment>
106106
)}
107-
107+
108108
</div>
109109
</div>
110110
);

src/components/UserMenu/index.jsx

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import React, { useState, useCallback, Fragment } from "react";
77
import Avatar from "../Avatar";
88
import cn from "classnames";
99
import OutsideClickHandler from "react-outside-click-handler";
10-
import config from "../../../config";
10+
import { logout, getLogoutUrl } from "../../utils";
1111
import "./styles.css";
1212
import { useMediaQuery } from "react-responsive";
1313

@@ -17,18 +17,19 @@ const UserMenu = ({ profile }) => {
1717
const closeMenu = useCallback(() => {
1818
setIsOpenMenu(false);
1919
}, [setIsOpenMenu]);
20-
20+
2121
const isMobile = useMediaQuery({
2222
query: "(max-width: 1023px)",
2323
});
2424

2525
const toggleMenu = useCallback(() => {
2626
setIsOpenMenu(!isOpenMenu);
2727
}, [isOpenMenu, setIsOpenMenu]);
28-
29-
const logoutUrl = `${config.URL.AUTH}/logout?retUrl=${encodeURIComponent(
30-
"https://" + window.location.host
31-
)}`;
28+
29+
const onLogoutClick = useCallback((evt) => {
30+
evt.preventDefault();
31+
logout();
32+
}, []);
3233

3334
return (
3435
<OutsideClickHandler onOutsideClick={closeMenu}>
@@ -43,7 +44,7 @@ const UserMenu = ({ profile }) => {
4344
>
4445
<Avatar profile={profile} />
4546
{isMobile ? (<Fragment></Fragment>) : (<div className="user-menu-handle">{profile.handle}</div>) }
46-
47+
4748
</div>
4849

4950
{isOpenMenu && (
@@ -59,7 +60,7 @@ const UserMenu = ({ profile }) => {
5960
<div className="user-menu-popover-content">
6061
<ul className="user-menu-list">
6162
<li>
62-
<a href={logoutUrl}>Log Out</a>
63+
<a href={getLogoutUrl()} onClick={onLogoutClick}>Log Out</a>
6364
</li>
6465
</ul>
6566
</div>

0 commit comments

Comments
 (0)