Skip to content

Commit c35df7e

Browse files
Carlos MunizCarlos Muniz
andauthored
feat: Add StudioHeader with optional AppMenu (#199)
* feat: Add StudioHeader with optional AppMenu StudioHeader is a graft of Header with an additional optional AppMenu. Some Frontend Apps use Menus in their custom headers to provide more functionality in their apps. By adding this functionality in StudioHeader, it will be easier for frontend apps in Studio to adopt this component without it affecting the main Header component. * test: Add tests for StudioHeader and AppMenu * fix: Remove orderHistory * fix: Remove Responsive components * fix: Redefine User Menu for Studio The userMenu in StudioHeader will be used more for Studio related items such as Studio Home and Studio Maintenance. This requires new messages and reestablishing the url destinations of these menu items. * fix: Remove loggedOutItems * fix: Remove AUTHN_MINIMAL_HEADER items * fix: Remove unnecessary tests Anonymous sessions do not exist in the Studio. And Studio is not Mobile Ready. Tests of these kind are superfluous and have been removed. * feat: Turn mainMenu into an optional prop * test: Add test for optional mainMenu prop * feat: Update snapshots * fix: Remove ResponsiveContext * fix: Remove href and update appMenu prop Dropping the href because having a link that also works as a dropdown can be mildly confusing. Changing menu (type, href, content ) triplet to stick to the pattern; so we removed "menu". Also adding brackets around the triplet. Lastly, updating test and snapshot. Co-authored-by: Carlos Muniz <[email protected]>
1 parent a8d5a0b commit c35df7e

File tree

7 files changed

+719
-1
lines changed

7 files changed

+719
-1
lines changed

.env.development

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ CSRF_TOKEN_API_PATH=/csrf/api/v1/token
55
ECOMMERCE_BASE_URL=http://localhost:18130
66
LANGUAGE_PREFERENCE_COOKIE_NAME=openedx-language-preference
77
LMS_BASE_URL=http://localhost:18000
8+
STUDIO_BASE_URL=http://localhost:18010
89
LOGIN_URL=http://localhost:18000/login
910
LOGOUT_URL=http://localhost:18000/logout
1011
MARKETING_SITE_BASE_URL=http://localhost:18000

src/DesktopHeader.jsx

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,24 @@ class DesktopHeader extends React.Component {
5454
});
5555
}
5656

57+
// Renders an optional App Menu for
58+
renderAppMenu() {
59+
const { appMenu } = this.props;
60+
const { content: appMenuContent, menuItems } = appMenu;
61+
return (
62+
<Menu transitionClassName="menu-dropdown" transitionTimeout={250}>
63+
<MenuTrigger tag="a" className="nav-link d-inline-flex align-items-center">
64+
{appMenuContent} <CaretIcon role="img" aria-hidden focusable="false" />
65+
</MenuTrigger>
66+
<MenuContent className="mb-0 dropdown-menu show dropdown-menu-right pin-right shadow py-2">
67+
{menuItems.map(({ type, href, content }) => (
68+
<a className={`dropdown-${type}`} key={`${type}-${content}`} href={href}>{content}</a>
69+
))}
70+
</MenuContent>
71+
</Menu>
72+
);
73+
}
74+
5775
renderUserMenu() {
5876
const {
5977
userMenu,
@@ -102,6 +120,7 @@ class DesktopHeader extends React.Component {
102120
logoDestination,
103121
loggedIn,
104122
intl,
123+
appMenu,
105124
} = this.props;
106125
const logoProps = { src: logo, alt: logoAltText, href: logoDestination };
107126
const logoClasses = getConfig().AUTHN_MINIMAL_HEADER ? 'mw-100' : null;
@@ -118,6 +137,14 @@ class DesktopHeader extends React.Component {
118137
>
119138
{this.renderMainMenu()}
120139
</nav>
140+
{appMenu ? (
141+
<nav
142+
aria-label={intl.formatMessage(messages['header.label.app.nav'])}
143+
className="nav app-nav"
144+
>
145+
{this.renderAppMenu()}
146+
</nav>
147+
) : null}
121148
<nav
122149
aria-label={intl.formatMessage(messages['header.label.secondary.nav'])}
123150
className="nav secondary-menu-container align-items-center ml-auto"
@@ -155,6 +182,20 @@ DesktopHeader.propTypes = {
155182

156183
// i18n
157184
intl: intlShape.isRequired,
185+
186+
// appMenu
187+
appMenu: PropTypes.shape(
188+
{
189+
content: PropTypes.string,
190+
menuItems: PropTypes.arrayOf(
191+
PropTypes.shape({
192+
type: PropTypes.string,
193+
href: PropTypes.string,
194+
content: PropTypes.string,
195+
}),
196+
),
197+
},
198+
),
158199
};
159200

160201
DesktopHeader.defaultProps = {
@@ -167,6 +208,7 @@ DesktopHeader.defaultProps = {
167208
avatar: null,
168209
username: null,
169210
loggedIn: false,
211+
appMenu: null,
170212
};
171213

172214
export default injectIntl(DesktopHeader);

src/Header.messages.jsx

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,16 @@ const messages = defineMessages({
5656
defaultMessage: 'Sign Up',
5757
description: 'Link to registration',
5858
},
59+
'header.user.menu.studio.home': {
60+
id: 'header.user.menu.studio.home',
61+
defaultMessage: 'Studio Home',
62+
description: 'Link to the Studio Home',
63+
},
64+
'header.user.menu.studio.maintenance': {
65+
id: 'header.user.menu.studio.maintenance',
66+
defaultMessage: 'Maintenance',
67+
description: 'Link to the Studio Maintenance',
68+
},
5969
'header.label.account.nav': {
6070
id: 'header.label.account.nav',
6171
defaultMessage: 'Account',
@@ -96,6 +106,11 @@ const messages = defineMessages({
96106
defaultMessage: 'Skip to main content',
97107
description: 'A link used by screen readers to allow users to skip to the main content of the page.',
98108
},
109+
'header.label.app.nav': {
110+
id: 'header.label.app.nav',
111+
defaultMessage: 'App',
112+
description: 'The aria label for the app Nav',
113+
},
99114
});
100115

101116
export default messages;

src/StudioHeader.jsx

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
import React, { useContext } from 'react';
2+
import PropTypes from 'prop-types';
3+
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
4+
import { AppContext } from '@edx/frontend-platform/react';
5+
import {
6+
APP_CONFIG_INITIALIZED,
7+
ensureConfig,
8+
mergeConfig,
9+
subscribe,
10+
} from '@edx/frontend-platform';
11+
12+
import DesktopHeader from './DesktopHeader';
13+
14+
import messages from './Header.messages';
15+
16+
ensureConfig([
17+
'STUDIO_BASE_URL',
18+
'LOGOUT_URL',
19+
'LOGIN_URL',
20+
'SITE_NAME',
21+
'LOGO_URL',
22+
'ORDER_HISTORY_URL',
23+
], 'StudioHeader component');
24+
25+
subscribe(APP_CONFIG_INITIALIZED, () => {
26+
mergeConfig({
27+
AUTHN_MINIMAL_HEADER: !!process.env.AUTHN_MINIMAL_HEADER,
28+
}, 'StudioHeader additional config');
29+
});
30+
31+
function StudioHeader({ intl, mainMenu, appMenu }) {
32+
const { authenticatedUser, config } = useContext(AppContext);
33+
34+
const userMenu = authenticatedUser === null ? [] : [
35+
{
36+
type: 'item',
37+
href: `${config.STUDIO_BASE_URL}`,
38+
content: intl.formatMessage(messages['header.user.menu.studio.home']),
39+
},
40+
{
41+
type: 'item',
42+
href: `${config.STUDIO_BASE_URL}/maintenance`,
43+
content: intl.formatMessage(messages['header.user.menu.studio.maintenance']),
44+
},
45+
{
46+
type: 'item',
47+
href: config.LOGOUT_URL,
48+
content: intl.formatMessage(messages['header.user.menu.logout']),
49+
},
50+
];
51+
52+
const props = {
53+
logo: config.LOGO_URL,
54+
logoAltText: config.SITE_NAME,
55+
logoDestination: config.STUDIO_BASE_URL,
56+
loggedIn: authenticatedUser !== null,
57+
username: authenticatedUser !== null ? authenticatedUser.username : null,
58+
avatar: authenticatedUser !== null ? authenticatedUser.avatar : null,
59+
mainMenu,
60+
userMenu,
61+
appMenu,
62+
loggedOutItems: [],
63+
};
64+
65+
return <DesktopHeader {...props} />;
66+
}
67+
68+
StudioHeader.propTypes = {
69+
intl: intlShape.isRequired,
70+
appMenu: PropTypes.shape(
71+
{
72+
content: PropTypes.string,
73+
href: PropTypes.string,
74+
menuItems: PropTypes.arrayOf(
75+
PropTypes.shape({
76+
type: PropTypes.string,
77+
href: PropTypes.string,
78+
content: PropTypes.string,
79+
}),
80+
),
81+
},
82+
),
83+
mainMenu: PropTypes.arrayOf(
84+
PropTypes.shape(
85+
{
86+
type: PropTypes.string,
87+
href: PropTypes.string,
88+
content: PropTypes.string,
89+
},
90+
),
91+
),
92+
};
93+
94+
StudioHeader.defaultProps = {
95+
appMenu: null,
96+
mainMenu: [],
97+
};
98+
99+
export default injectIntl(StudioHeader);

src/StudioHeader.test.jsx

Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
import React from 'react';
2+
import { IntlProvider } from '@edx/frontend-platform/i18n';
3+
import TestRenderer from 'react-test-renderer';
4+
import { AppContext } from '@edx/frontend-platform/react';
5+
6+
import { StudioHeader } from './index';
7+
8+
describe('<StudioHeader />', () => {
9+
it('renders correctly', () => {
10+
const component = (
11+
<IntlProvider locale="en" messages={{}}>
12+
<AppContext.Provider
13+
value={{
14+
authenticatedUser: {
15+
userId: 'abc123',
16+
username: 'edX',
17+
roles: [],
18+
administrator: false,
19+
},
20+
config: {
21+
STUDIO_BASE_URL: process.env.STUDIO_BASE_URL,
22+
SITE_NAME: process.env.SITE_NAME,
23+
LOGIN_URL: process.env.LOGIN_URL,
24+
LOGOUT_URL: process.env.LOGOUT_URL,
25+
LOGO_URL: process.env.LOGO_URL,
26+
},
27+
}}
28+
>
29+
<StudioHeader />
30+
</AppContext.Provider>
31+
</IntlProvider>
32+
);
33+
34+
const wrapper = TestRenderer.create(component);
35+
36+
expect(wrapper.toJSON()).toMatchSnapshot();
37+
});
38+
39+
it('renders correctly with the optional app menu', () => {
40+
const appMenu = {
41+
content: 'App Menu',
42+
menuItems: [
43+
{
44+
type: 'dropdown',
45+
href: 'https://menu-href-url.org',
46+
content: 'Content 1',
47+
},
48+
{
49+
type: 'dropdown',
50+
href: 'https://menu-href-url.org',
51+
content: 'Content 2',
52+
},
53+
{
54+
type: 'dropdown',
55+
href: 'https://menu-href-url.org',
56+
content: 'Content 3',
57+
},
58+
],
59+
};
60+
const component = (
61+
<IntlProvider locale="en" messages={{}}>
62+
<AppContext.Provider
63+
value={{
64+
authenticatedUser: {
65+
userId: 'abc123',
66+
username: 'edX',
67+
roles: [],
68+
administrator: false,
69+
},
70+
config: {
71+
STUDIO_BASE_URL: process.env.STUDIO_BASE_URL,
72+
SITE_NAME: process.env.SITE_NAME,
73+
LOGIN_URL: process.env.LOGIN_URL,
74+
LOGOUT_URL: process.env.LOGOUT_URL,
75+
LOGO_URL: process.env.LOGO_URL,
76+
},
77+
}}
78+
>
79+
<StudioHeader appMenu={appMenu} />
80+
</AppContext.Provider>
81+
</IntlProvider>
82+
);
83+
84+
const wrapper = TestRenderer.create(component);
85+
86+
expect(wrapper.toJSON()).toMatchSnapshot();
87+
});
88+
89+
it('renders correctly with the optional main menu', () => {
90+
const mainMenu = [
91+
{
92+
type: 'dropdown',
93+
href: 'https://menu-href-url.org',
94+
content: 'Content 1',
95+
},
96+
{
97+
type: 'dropdown',
98+
href: 'https://menu-href-url.org',
99+
content: 'Content 2',
100+
},
101+
{
102+
type: 'dropdown',
103+
href: 'https://menu-href-url.org',
104+
content: 'Content 3',
105+
},
106+
];
107+
const component = (
108+
<IntlProvider locale="en" messages={{}}>
109+
<AppContext.Provider
110+
value={{
111+
authenticatedUser: {
112+
userId: 'abc123',
113+
username: 'edX',
114+
roles: [],
115+
administrator: false,
116+
},
117+
config: {
118+
STUDIO_BASE_URL: process.env.STUDIO_BASE_URL,
119+
SITE_NAME: process.env.SITE_NAME,
120+
LOGIN_URL: process.env.LOGIN_URL,
121+
LOGOUT_URL: process.env.LOGOUT_URL,
122+
LOGO_URL: process.env.LOGO_URL,
123+
},
124+
}}
125+
>
126+
<StudioHeader mainMenu={mainMenu} />
127+
</AppContext.Provider>
128+
</IntlProvider>
129+
);
130+
131+
const wrapper = TestRenderer.create(component);
132+
133+
expect(wrapper.toJSON()).toMatchSnapshot();
134+
});
135+
});

0 commit comments

Comments
 (0)