Compare commits

...

32 Commits

Author SHA1 Message Date
renovate[bot]
85e8094833 fix(deps): update font awesome to v6 (#281)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-02-06 14:17:48 +05:00
dependabot[bot]
aff8dda3ee chore(deps): bump decode-uri-component from 0.2.0 to 0.2.2 (#287)
Bumps [decode-uri-component](https://github.com/SamVerschueren/decode-uri-component) from 0.2.0 to 0.2.2.
- [Release notes](https://github.com/SamVerschueren/decode-uri-component/releases)
- [Commits](https://github.com/SamVerschueren/decode-uri-component/compare/v0.2.0...v0.2.2)

---
updated-dependencies:
- dependency-name: decode-uri-component
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-02-01 17:15:00 +05:00
dependabot[bot]
51b505552d chore(deps): bump cookiejar from 2.1.3 to 2.1.4 (#296)
Bumps [cookiejar](https://github.com/bmeck/node-cookiejar) from 2.1.3 to 2.1.4.
- [Release notes](https://github.com/bmeck/node-cookiejar/releases)
- [Commits](https://github.com/bmeck/node-cookiejar/commits)

---
updated-dependencies:
- dependency-name: cookiejar
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-02-01 17:14:36 +05:00
dependabot[bot]
3648f1b6be build(deps): bump json5 from 1.0.1 to 1.0.2
Bumps [json5](https://github.com/json5/json5) from 1.0.1 to 1.0.2.
- [Release notes](https://github.com/json5/json5/releases)
- [Changelog](https://github.com/json5/json5/blob/main/CHANGELOG.md)
- [Commits](https://github.com/json5/json5/compare/v1.0.1...v1.0.2)

---
updated-dependencies:
- dependency-name: json5
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-01-30 14:12:31 +00:00
Bilal Qamar
c78b6964b9 chore: updated frontend-build version to v12.4.19 (#297) 2023-01-25 18:51:25 +05:00
renovate[bot]
664d05134b fix(deps): update dependency @edx/paragon to v20.27.0 2023-01-23 10:38:54 +00:00
renovate[bot]
b969522cd0 chore(deps): update dependency @edx/frontend-build to v12.4.16 2023-01-23 10:34:46 +00:00
renovate[bot]
0cd8210ea7 chore(deps): update dependency @testing-library/dom to v8.19.1 2023-01-16 10:17:13 +00:00
renovate[bot]
1c763c2102 chore(deps): update dependency @edx/frontend-build to v12.4.15 2023-01-09 09:27:54 +00:00
Mashal Malik
073003284a Moving code coverage from codecov package to CI (#289)
* fix: removed derpeciated package codecov

* fix: install edx/paragon 20.20.0 fixed version

* fix: specified paragron 20.20.0 version

Co-authored-by: Shahroz Ahmad <shahroz.ahmad@arbisoft.com>
2022-12-29 12:25:49 +05:00
Bilal Qamar
92fdf85c9a feat: paragon updated to v20 & frontend-build version updated
* feat: paragon updated to v20 & frontend-build version updated

* refactor: moved paragon from devDependencies to satisfy eslint rule

* refactor: updated snapshots
2022-12-09 15:57:09 +05:00
Sagirov Eugeniy
5ee8a8c75c feat: Account pages. Updated menu items urls. 2022-12-02 12:28:15 +00:00
Abdullah Waheed
536d67404f refactor: updated renovate config to auto update minor and patch versions of edx dependencies 2022-11-30 13:17:21 +00:00
Bilal Qamar
9d99bfcec6 refactor: updated snapshots 2022-11-25 16:53:50 +05:00
Bilal Qamar
3180c9d973 refactor: moved paragon from devDependencies to satisfy eslint rule 2022-11-25 16:48:29 +05:00
Bilal Qamar
1645274d9f feat: paragon updated to v20 & frontend-build version updated 2022-11-25 16:36:06 +05:00
Bilal Qamar
84e43cb038 refactor: bumped loader-utils 2022-11-25 16:26:11 +05:00
julianajlk
994b21c0c1 fix: Change frontend-platform peer dependency to v2 or v3 range 2022-11-14 20:19:37 +00:00
renovate[bot]
940b45ba7e chore(deps): update dependency jest to v29 2022-11-14 07:52:30 +00:00
dependabot[bot]
4efa0a07ae build(deps): bump loader-utils from 1.4.0 to 1.4.1 (#278)
Bumps [loader-utils](https://github.com/webpack/loader-utils) from 1.4.0 to 1.4.1.
- [Release notes](https://github.com/webpack/loader-utils/releases)
- [Changelog](https://github.com/webpack/loader-utils/blob/v1.4.1/CHANGELOG.md)
- [Commits](https://github.com/webpack/loader-utils/compare/v1.4.0...v1.4.1)

---
updated-dependencies:
- dependency-name: loader-utils
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-11-10 17:34:00 +05:00
renovate[bot]
2bd6879bda chore(deps): update actions/checkout action to v3 (#211)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-11-10 17:18:45 +05:00
Muhammad Abdullah Waheed
b479f0b376 Merge pull request #205 from openedx/dependabot/npm_and_yarn/async-2.6.4
build(deps): bump async from 2.6.3 to 2.6.4
2022-11-08 18:40:15 +05:00
Leangseu Kim
dfdcbc0a8d feat: upgrade frontend platform to version 3 2022-11-07 12:45:43 +00:00
renovate[bot]
c3b02a2946 chore(deps): update dependency enzyme-adapter-react-16 to v1.15.7 2022-11-07 09:36:45 +00:00
renovate[bot]
f6c1a8bcc1 chore(deps): update dependency redux-saga to v1.2.1 2022-10-31 07:42:11 +00:00
Bilal Qamar
6c02962e0d refactor: updated frontend-build & resolved eslint issues 2022-10-26 10:37:57 -03:00
renovate[bot]
acaf98f0b1 chore(deps): update dependency @testing-library/dom to v8.19.0 2022-10-24 08:15:34 +00:00
Adolfo R. Brandes
90351083aa Merge pull request #256 from openedx/abdullahwaheed/transifex-languages-list-update
Supported Transifex languages in Makefile
2022-10-20 16:15:57 -03:00
Adolfo R. Brandes
6f75684ad9 Merge pull request #271 from brian-smith-tcril/studio-header-component
refactor: make studio header more flexible
2022-10-20 14:24:44 -03:00
Brian Smith
a54f099d68 refactor: make studio header more flexible 2022-10-19 10:20:10 -04:00
Abdullah Waheed
de9eb63b07 feat: added new translations in Makefile and updated all the translations 2022-09-06 20:05:09 +05:00
dependabot[bot]
64f55150b6 build(deps): bump async from 2.6.3 to 2.6.4
Bumps [async](https://github.com/caolan/async) from 2.6.3 to 2.6.4.
- [Release notes](https://github.com/caolan/async/releases)
- [Changelog](https://github.com/caolan/async/blob/v2.6.4/CHANGELOG.md)
- [Commits](https://github.com/caolan/async/compare/v2.6.3...v2.6.4)

---
updated-dependencies:
- dependency-name: async
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-04-18 11:56:29 +00:00
37 changed files with 12065 additions and 15700 deletions

View File

@@ -1,4 +1,6 @@
ACCESS_TOKEN_COOKIE_NAME=edx-jwt-cookie-header-payload ACCESS_TOKEN_COOKIE_NAME=edx-jwt-cookie-header-payload
ACCOUNT_PROFILE_URL=http://localhost:1995
ACCOUNT_SETTINGS_URL=http://localhost:1997
BASE_URL=localhost:8080 BASE_URL=localhost:8080
CREDENTIALS_BASE_URL=http://localhost:18150 CREDENTIALS_BASE_URL=http://localhost:18150
CSRF_TOKEN_API_PATH=/csrf/api/v1/token CSRF_TOKEN_API_PATH=/csrf/api/v1/token

View File

@@ -1,3 +1,4 @@
// eslint-disable-next-line import/no-extraneous-dependencies
const { createConfig } = require('@edx/frontend-build'); const { createConfig } = require('@edx/frontend-build');
module.exports = createConfig('eslint'); module.exports = createConfig('eslint');

View File

@@ -14,7 +14,7 @@ jobs:
node: [16] node: [16]
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v2 uses: actions/checkout@v3
with: with:
fetch-depth: 0 fetch-depth: 0
- name: Setup Nodejs - name: Setup Nodejs
@@ -34,4 +34,4 @@ jobs:
- name: i18n_extract - name: i18n_extract
run: npm run i18n_extract run: npm run i18n_extract
- name: Coverage - name: Coverage
uses: codecov/codecov-action@v2 uses: codecov/codecov-action@v3

View File

@@ -9,7 +9,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v2 uses: actions/checkout@v3
with: with:
fetch-depth: 0 fetch-depth: 0
- name: Setup Node.js - name: Setup Node.js
@@ -27,7 +27,7 @@ jobs:
- name: i18n_extract - name: i18n_extract
run: npm run i18n_extract run: npm run i18n_extract
- name: Coverage - name: Coverage
uses: codecov/codecov-action@v2 uses: codecov/codecov-action@v3
- name: Build - name: Build
run: npm run build run: npm run build
- name: Release - name: Release

2
.gitignore vendored
View File

@@ -7,3 +7,5 @@ temp
src/i18n/transifex_input.json src/i18n/transifex_input.json
module.config.js module.config.js
.idea/ .idea/
.vscode

View File

@@ -1,5 +1,5 @@
transifex_resource = frontend-component-header transifex_resource = frontend-component-header
transifex_langs = "ar,fr,fr_CA,es_419,zh_CN" transifex_langs = "ar,fr,es_419,zh_CN,pt,it,de,uk,ru,hi,fr_CA"
transifex_utils = ./node_modules/.bin/transifex-utils.js transifex_utils = ./node_modules/.bin/transifex-utils.js
i18n = ./src/i18n i18n = ./src/i18n

View File

@@ -26,6 +26,8 @@ Environment Variables
Defaults to "localhost" in development. Defaults to "localhost" in development.
* ``LOGO_URL`` - The URL of the site's logo. This logo is displayed in the header. * ``LOGO_URL`` - The URL of the site's logo. This logo is displayed in the header.
* ``ORDER_HISTORY_URL`` - The URL of the order history page. * ``ORDER_HISTORY_URL`` - The URL of the order history page.
* ``ACCOUNT_PROFILE_URL`` - The URL of the account profile page.
* ``ACCOUNT_SETTINGS_URL`` - The URL of the account settings page.
* ``AUTHN_MINIMAL_HEADER`` - A boolean flag which hides the main menu, user menu, and logged-out * ``AUTHN_MINIMAL_HEADER`` - A boolean flag which hides the main menu, user menu, and logged-out
menu items when truthy. This is intended to be used in micro-frontends like menu items when truthy. This is intended to be used in micro-frontends like
frontend-app-authentication in which these menus are considered distractions from the user's task. frontend-app-authentication in which these menus are considered distractions from the user's task.

26331
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -34,13 +34,17 @@
"homepage": "https://github.com/openedx/frontend-component-header#readme", "homepage": "https://github.com/openedx/frontend-component-header#readme",
"devDependencies": { "devDependencies": {
"@edx/brand": "npm:@edx/brand-openedx@1.1.0", "@edx/brand": "npm:@edx/brand-openedx@1.1.0",
"@edx/frontend-build": "11.0.2", "@edx/browserslist-config": "^1.1.1",
"@edx/frontend-platform": "2.6.2", "@edx/frontend-build": "^12.4.19",
"@edx/paragon": "19.25.3", "@edx/frontend-platform": "^3.0.1",
"codecov": "3.8.3", "@testing-library/dom": "8.19.1",
"@testing-library/jest-dom": "5.16.5",
"@testing-library/react": "10.4.9",
"enzyme": "3.11.0", "enzyme": "3.11.0",
"enzyme-adapter-react-16": "1.15.6", "enzyme-adapter-react-16": "1.15.7",
"husky": "7.0.4", "husky": "7.0.4",
"jest": "29.3.1",
"jest-chain": "1.1.6",
"prop-types": "15.8.1", "prop-types": "15.8.1",
"react": "16.14.0", "react": "16.14.0",
"react-dom": "16.14.0", "react-dom": "16.14.0",
@@ -49,26 +53,21 @@
"react-test-renderer": "16.14.0", "react-test-renderer": "16.14.0",
"reactifex": "1.1.1", "reactifex": "1.1.1",
"redux": "4.2.0", "redux": "4.2.0",
"redux-saga": "1.1.3", "redux-saga": "1.2.1"
"@testing-library/dom": "8.18.1",
"@testing-library/jest-dom": "5.16.5",
"jest": "28.1.3",
"jest-chain": "1.1.6",
"@testing-library/react": "10.4.9"
}, },
"dependencies": { "dependencies": {
"@edx/paragon": "20.27.0",
"@fortawesome/fontawesome-svg-core": "6.2.1",
"@fortawesome/free-brands-svg-icons": "6.2.1",
"@fortawesome/free-regular-svg-icons": "6.2.1",
"@fortawesome/free-solid-svg-icons": "6.2.1",
"@fortawesome/react-fontawesome": "^0.2.0",
"babel-polyfill": "6.26.0", "babel-polyfill": "6.26.0",
"react-responsive": "8.2.0", "react-responsive": "8.2.0",
"react-transition-group": "4.4.5", "react-transition-group": "4.4.5"
"@fortawesome/fontawesome-svg-core": "1.2.36",
"@fortawesome/free-brands-svg-icons": "5.15.4",
"@fortawesome/free-regular-svg-icons": "5.15.4",
"@fortawesome/free-solid-svg-icons": "5.15.4",
"@fortawesome/react-fontawesome": "^0.2.0"
}, },
"peerDependencies": { "peerDependencies": {
"@edx/frontend-platform": "^2.0.0", "@edx/frontend-platform": "^2.0.0 || ^3.0.0",
"@edx/paragon": ">= 7.0.0 < 21.0.0",
"prop-types": "^15.5.10", "prop-types": "^15.5.10",
"react": "^16.9.0", "react": "^16.9.0",
"react-dom": "^16.9.0" "react-dom": "^16.9.0"

View File

@@ -22,6 +22,11 @@
"pin" "pin"
], ],
"automerge": true "automerge": true
},
{
"matchPackagePatterns": ["@edx"],
"matchUpdateTypes": ["minor", "patch"],
"automerge": true
} }
], ],
"timezone": "America/New_York" "timezone": "America/New_York"

View File

@@ -3,12 +3,12 @@ import PropTypes from 'prop-types';
import { AvatarIcon } from './Icons'; import { AvatarIcon } from './Icons';
function Avatar({ const Avatar = ({
size, size,
src, src,
alt, alt,
className, className,
}) { }) => {
const avatar = src ? ( const avatar = src ? (
<img className="d-block w-100 h-100" src={src} alt={alt} /> <img className="d-block w-100 h-100" src={src} alt={alt} />
) : ( ) : (
@@ -23,7 +23,7 @@ function Avatar({
{avatar} {avatar}
</span> </span>
); );
} };
Avatar.propTypes = { Avatar.propTypes = {
src: PropTypes.string, src: PropTypes.string,

View File

@@ -30,7 +30,7 @@ subscribe(APP_CONFIG_INITIALIZED, () => {
}, 'Header additional config'); }, 'Header additional config');
}); });
function Header({ intl }) { const Header = ({ intl }) => {
const { authenticatedUser, config } = useContext(AppContext); const { authenticatedUser, config } = useContext(AppContext);
const mainMenu = [ const mainMenu = [
@@ -55,12 +55,12 @@ function Header({ intl }) {
}, },
{ {
type: 'item', type: 'item',
href: `${config.LMS_BASE_URL}/u/${authenticatedUser.username}`, href: `${config.ACCOUNT_PROFILE_URL}/u/${authenticatedUser.username}`,
content: intl.formatMessage(messages['header.user.menu.profile']), content: intl.formatMessage(messages['header.user.menu.profile']),
}, },
{ {
type: 'item', type: 'item',
href: `${config.LMS_BASE_URL}/account/settings`, href: config.ACCOUNT_SETTINGS_URL,
content: intl.formatMessage(messages['header.user.menu.account.settings']), content: intl.formatMessage(messages['header.user.menu.account.settings']),
}, },
{ {
@@ -110,7 +110,7 @@ function Header({ intl }) {
</Responsive> </Responsive>
</> </>
); );
} };
Header.propTypes = { Header.propTypes = {
intl: intlShape.isRequired, intl: intlShape.isRequired,

View File

@@ -1,3 +1,4 @@
/* eslint-disable react/prop-types */
import React from 'react'; import React from 'react';
import { IntlProvider } from '@edx/frontend-platform/i18n'; import { IntlProvider } from '@edx/frontend-platform/i18n';
import TestRenderer from 'react-test-renderer'; import TestRenderer from 'react-test-renderer';
@@ -6,28 +7,31 @@ import { Context as ResponsiveContext } from 'react-responsive';
import Header from './index'; import Header from './index';
const HeaderComponent = ({ width, contextValue }) => (
<ResponsiveContext.Provider value={width}>
<IntlProvider locale="en" messages={{}}>
<AppContext.Provider
value={contextValue}
>
<Header />
</AppContext.Provider>
</IntlProvider>
</ResponsiveContext.Provider>
);
describe('<Header />', () => { describe('<Header />', () => {
it('renders correctly for anonymous desktop', () => { it('renders correctly for anonymous desktop', () => {
const component = ( const contextValue = {
<ResponsiveContext.Provider value={{ width: 1280 }}> authenticatedUser: null,
<IntlProvider locale="en" messages={{}}> config: {
<AppContext.Provider LMS_BASE_URL: process.env.LMS_BASE_URL,
value={{ SITE_NAME: process.env.SITE_NAME,
authenticatedUser: null, LOGIN_URL: process.env.LOGIN_URL,
config: { LOGOUT_URL: process.env.LOGOUT_URL,
LMS_BASE_URL: process.env.LMS_BASE_URL, LOGO_URL: process.env.LOGO_URL,
SITE_NAME: process.env.SITE_NAME, },
LOGIN_URL: process.env.LOGIN_URL, };
LOGOUT_URL: process.env.LOGOUT_URL, const component = <HeaderComponent width={{ width: 1280 }} contextValue={contextValue} />;
LOGO_URL: process.env.LOGO_URL,
},
}}
>
<Header />
</AppContext.Provider>
</IntlProvider>
</ResponsiveContext.Provider>
);
const wrapper = TestRenderer.create(component); const wrapper = TestRenderer.create(component);
@@ -35,31 +39,22 @@ describe('<Header />', () => {
}); });
it('renders correctly for authenticated desktop', () => { it('renders correctly for authenticated desktop', () => {
const component = ( const contextValue = {
<ResponsiveContext.Provider value={{ width: 1280 }}> authenticatedUser: {
<IntlProvider locale="en" messages={{}}> userId: 'abc123',
<AppContext.Provider username: 'edX',
value={{ roles: [],
authenticatedUser: { administrator: false,
userId: 'abc123', },
username: 'edX', config: {
roles: [], LMS_BASE_URL: process.env.LMS_BASE_URL,
administrator: false, SITE_NAME: process.env.SITE_NAME,
}, LOGIN_URL: process.env.LOGIN_URL,
config: { LOGOUT_URL: process.env.LOGOUT_URL,
LMS_BASE_URL: process.env.LMS_BASE_URL, LOGO_URL: process.env.LOGO_URL,
SITE_NAME: process.env.SITE_NAME, },
LOGIN_URL: process.env.LOGIN_URL, };
LOGOUT_URL: process.env.LOGOUT_URL, const component = <HeaderComponent width={{ width: 1280 }} contextValue={contextValue} />;
LOGO_URL: process.env.LOGO_URL,
},
}}
>
<Header />
</AppContext.Provider>
</IntlProvider>
</ResponsiveContext.Provider>
);
const wrapper = TestRenderer.create(component); const wrapper = TestRenderer.create(component);
@@ -67,26 +62,17 @@ describe('<Header />', () => {
}); });
it('renders correctly for anonymous mobile', () => { it('renders correctly for anonymous mobile', () => {
const component = ( const contextValue = {
<ResponsiveContext.Provider value={{ width: 500 }}> authenticatedUser: null,
<IntlProvider locale="en" messages={{}}> config: {
<AppContext.Provider LMS_BASE_URL: process.env.LMS_BASE_URL,
value={{ SITE_NAME: process.env.SITE_NAME,
authenticatedUser: null, LOGIN_URL: process.env.LOGIN_URL,
config: { LOGOUT_URL: process.env.LOGOUT_URL,
LMS_BASE_URL: process.env.LMS_BASE_URL, LOGO_URL: process.env.LOGO_URL,
SITE_NAME: process.env.SITE_NAME, },
LOGIN_URL: process.env.LOGIN_URL, };
LOGOUT_URL: process.env.LOGOUT_URL, const component = <HeaderComponent width={{ width: 500 }} contextValue={contextValue} />;
LOGO_URL: process.env.LOGO_URL,
},
}}
>
<Header />
</AppContext.Provider>
</IntlProvider>
</ResponsiveContext.Provider>
);
const wrapper = TestRenderer.create(component); const wrapper = TestRenderer.create(component);
@@ -94,31 +80,22 @@ describe('<Header />', () => {
}); });
it('renders correctly for authenticated mobile', () => { it('renders correctly for authenticated mobile', () => {
const component = ( const contextValue = {
<ResponsiveContext.Provider value={{ width: 500 }}> authenticatedUser: {
<IntlProvider locale="en" messages={{}}> userId: 'abc123',
<AppContext.Provider username: 'edX',
value={{ roles: [],
authenticatedUser: { administrator: false,
userId: 'abc123', },
username: 'edX', config: {
roles: [], LMS_BASE_URL: process.env.LMS_BASE_URL,
administrator: false, SITE_NAME: process.env.SITE_NAME,
}, LOGIN_URL: process.env.LOGIN_URL,
config: { LOGOUT_URL: process.env.LOGOUT_URL,
LMS_BASE_URL: process.env.LMS_BASE_URL, LOGO_URL: process.env.LOGO_URL,
SITE_NAME: process.env.SITE_NAME, },
LOGIN_URL: process.env.LOGIN_URL, };
LOGOUT_URL: process.env.LOGOUT_URL, const component = <HeaderComponent width={{ width: 500 }} contextValue={contextValue} />;
LOGO_URL: process.env.LOGO_URL,
},
}}
>
<Header />
</AppContext.Provider>
</IntlProvider>
</ResponsiveContext.Provider>
);
const wrapper = TestRenderer.create(component); const wrapper = TestRenderer.create(component);

View File

@@ -1,6 +1,6 @@
import React from 'react'; import React from 'react';
export const MenuIcon = props => ( export const MenuIcon = (props) => (
<svg <svg
width="24px" width="24px"
height="24px" height="24px"
@@ -14,7 +14,7 @@ export const MenuIcon = props => (
</svg> </svg>
); );
export const AvatarIcon = props => ( export const AvatarIcon = (props) => (
<svg <svg
width="24px" width="24px"
height="24px" height="24px"
@@ -29,7 +29,7 @@ export const AvatarIcon = props => (
</svg> </svg>
); );
export const CaretIcon = props => ( export const CaretIcon = (props) => (
<svg <svg
width="16px" width="16px"
height="16px" height="16px"

View File

@@ -1,29 +1,25 @@
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
function Logo({ src, alt, ...attributes }) { const Logo = ({ src, alt, ...attributes }) => (
return ( <img src={src} alt={alt} {...attributes} />
<img src={src} alt={alt} {...attributes} /> );
);
}
Logo.propTypes = { Logo.propTypes = {
src: PropTypes.string.isRequired, src: PropTypes.string.isRequired,
alt: PropTypes.string.isRequired, alt: PropTypes.string.isRequired,
}; };
function LinkedLogo({ const LinkedLogo = ({
href, href,
src, src,
alt, alt,
...attributes ...attributes
}) { }) => (
return ( <a href={href} {...attributes}>
<a href={href} {...attributes}> <img className="d-block" src={src} alt={alt} />
<img className="d-block" src={src} alt={alt} /> </a>
</a> );
);
}
LinkedLogo.propTypes = { LinkedLogo.propTypes = {
href: PropTypes.string.isRequired, href: PropTypes.string.isRequired,

View File

@@ -2,12 +2,10 @@ import React from 'react';
import { CSSTransition } from 'react-transition-group'; import { CSSTransition } from 'react-transition-group';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
function MenuTrigger({ tag, className, ...attributes }) { const MenuTrigger = ({ tag, className, ...attributes }) => React.createElement(tag, {
return React.createElement(tag, { className: `menu-trigger ${className}`,
className: `menu-trigger ${className}`, ...attributes,
...attributes, });
});
}
MenuTrigger.propTypes = { MenuTrigger.propTypes = {
tag: PropTypes.string, tag: PropTypes.string,
className: PropTypes.string, className: PropTypes.string,
@@ -18,12 +16,10 @@ MenuTrigger.defaultProps = {
}; };
const MenuTriggerType = <MenuTrigger />.type; const MenuTriggerType = <MenuTrigger />.type;
function MenuContent({ tag, className, ...attributes }) { const MenuContent = ({ tag, className, ...attributes }) => React.createElement(tag, {
return React.createElement(tag, { className: ['menu-content', className].join(' '),
className: ['menu-content', className].join(' '), ...attributes,
...attributes, });
});
}
MenuContent.propTypes = { MenuContent.propTypes = {
tag: PropTypes.string, tag: PropTypes.string,
className: PropTypes.string, className: PropTypes.string,

View File

@@ -5,11 +5,17 @@ import { AppContext } from '@edx/frontend-platform/react';
import { import {
APP_CONFIG_INITIALIZED, APP_CONFIG_INITIALIZED,
ensureConfig, ensureConfig,
getConfig,
mergeConfig, mergeConfig,
subscribe, subscribe,
} from '@edx/frontend-platform'; } from '@edx/frontend-platform';
import { ActionRow } from '@edx/paragon';
import DesktopHeader from './DesktopHeader'; import { Menu, MenuTrigger, MenuContent } from './Menu';
import Avatar from './Avatar';
import { LinkedLogo, Logo } from './Logo';
import { CaretIcon } from './Icons';
import messages from './Header.messages'; import messages from './Header.messages';
@@ -28,7 +34,124 @@ subscribe(APP_CONFIG_INITIALIZED, () => {
}, 'StudioHeader additional config'); }, 'StudioHeader additional config');
}); });
function StudioHeader({ intl, mainMenu, appMenu }) { class StudioDesktopHeaderBase extends React.Component {
constructor(props) { // eslint-disable-line no-useless-constructor
super(props);
}
renderUserMenu() {
const {
userMenu,
avatar,
username,
intl,
} = this.props;
return (
<Menu transitionClassName="menu-dropdown" transitionTimeout={250}>
<MenuTrigger
tag="button"
aria-label={intl.formatMessage(messages['header.label.account.menu.for'], { username })}
className="btn btn-outline-primary d-inline-flex align-items-center pl-2 pr-3"
>
<Avatar size="1.5em" src={avatar} alt="" className="mr-2" />
{username} <CaretIcon role="img" aria-hidden focusable="false" />
</MenuTrigger>
<MenuContent className="mb-0 dropdown-menu show dropdown-menu-right pin-right shadow py-2">
{userMenu.map(({ type, href, content }) => (
<a className={`dropdown-${type}`} key={`${type}-${content}`} href={href}>{content}</a>
))}
</MenuContent>
</Menu>
);
}
renderLoggedOutItems() {
const { loggedOutItems } = this.props;
return loggedOutItems.map((item, i, arr) => (
<a
key={`${item.type}-${item.content}`}
className={i < arr.length - 1 ? 'btn mr-2 btn-link' : 'btn mr-2 btn-outline-primary'}
href={item.href}
>
{item.content}
</a>
));
}
render() {
const {
logo,
logoAltText,
logoDestination,
loggedIn,
intl,
actionRowContent,
} = this.props;
const logoProps = { src: logo, alt: logoAltText, href: logoDestination };
const logoClasses = getConfig().AUTHN_MINIMAL_HEADER ? 'mw-100' : null;
return (
<header className="site-header-desktop">
<a className="nav-skip sr-only sr-only-focusable" href="#main">{intl.formatMessage(messages['header.label.skip.nav'])}</a>
<div className={`container-fluid ${logoClasses}`}>
<div className="nav-container position-relative d-flex align-items-center">
{logoDestination === null ? <Logo className="logo" src={logo} alt={logoAltText} /> : <LinkedLogo className="logo" {...logoProps} />}
<ActionRow>
{actionRowContent}
<nav
aria-label={intl.formatMessage(messages['header.label.secondary.nav'])}
className="nav secondary-menu-container align-items-center ml-auto"
>
{loggedIn ? this.renderUserMenu() : this.renderLoggedOutItems()}
</nav>
</ActionRow>
</div>
</div>
</header>
);
}
}
StudioDesktopHeaderBase.propTypes = {
userMenu: PropTypes.arrayOf(PropTypes.shape({
type: PropTypes.oneOf(['item', 'menu']),
href: PropTypes.string,
content: PropTypes.string,
})),
loggedOutItems: PropTypes.arrayOf(PropTypes.shape({
type: PropTypes.oneOf(['item', 'menu']),
href: PropTypes.string,
content: PropTypes.string,
})),
logo: PropTypes.string,
logoAltText: PropTypes.string,
logoDestination: PropTypes.string,
avatar: PropTypes.string,
username: PropTypes.string,
loggedIn: PropTypes.bool,
actionRowContent: PropTypes.element,
// i18n
intl: intlShape.isRequired,
};
StudioDesktopHeaderBase.defaultProps = {
userMenu: [],
loggedOutItems: [],
logo: null,
logoAltText: null,
logoDestination: null,
avatar: null,
username: null,
loggedIn: false,
actionRowContent: null,
};
const StudioDesktopHeader = injectIntl(StudioDesktopHeaderBase);
const StudioHeader = ({ intl, actionRowContent }) => {
const { authenticatedUser, config } = useContext(AppContext); const { authenticatedUser, config } = useContext(AppContext);
const userMenu = authenticatedUser === null ? [] : [ const userMenu = authenticatedUser === null ? [] : [
@@ -56,44 +179,22 @@ function StudioHeader({ intl, mainMenu, appMenu }) {
loggedIn: authenticatedUser !== null, loggedIn: authenticatedUser !== null,
username: authenticatedUser !== null ? authenticatedUser.username : null, username: authenticatedUser !== null ? authenticatedUser.username : null,
avatar: authenticatedUser !== null ? authenticatedUser.avatar : null, avatar: authenticatedUser !== null ? authenticatedUser.avatar : null,
mainMenu, actionRowContent,
userMenu, userMenu,
appMenu,
loggedOutItems: [], loggedOutItems: [],
}; };
return <DesktopHeader {...props} />; return <StudioDesktopHeader {...props} />;
} };
StudioHeader.propTypes = { StudioHeader.propTypes = {
intl: intlShape.isRequired, intl: intlShape.isRequired,
appMenu: PropTypes.shape( actionRowContent: PropTypes.element,
{
content: PropTypes.string,
href: PropTypes.string,
menuItems: PropTypes.arrayOf(
PropTypes.shape({
type: PropTypes.string,
href: PropTypes.string,
content: PropTypes.string,
}),
),
},
),
mainMenu: PropTypes.arrayOf(
PropTypes.shape(
{
type: PropTypes.string,
href: PropTypes.string,
content: PropTypes.string,
},
),
),
}; };
StudioHeader.defaultProps = { StudioHeader.defaultProps = {
appMenu: null, // eslint-disable-next-line react/jsx-no-useless-fragment
mainMenu: [], actionRowContent: <></>,
}; };
export default injectIntl(StudioHeader); export default injectIntl(StudioHeader);

View File

@@ -1,133 +1,106 @@
import React from 'react'; /* eslint-disable react/prop-types */
import React, { useMemo } from 'react';
import { IntlProvider } from '@edx/frontend-platform/i18n'; import { IntlProvider } from '@edx/frontend-platform/i18n';
import TestRenderer from 'react-test-renderer'; import TestRenderer from 'react-test-renderer';
import { Link } from 'react-router-dom';
import { AppContext } from '@edx/frontend-platform/react'; import { AppContext } from '@edx/frontend-platform/react';
import {
ActionRow,
Button,
Dropdown,
} from '@edx/paragon';
import { StudioHeader } from './index'; import { StudioHeader } from './index';
const StudioHeaderComponent = ({ contextValue, appMenu = null, mainMenu = [] }) => (
<IntlProvider locale="en" messages={{}}>
<AppContext.Provider
value={contextValue}
>
<StudioHeader appMenu={appMenu} mainMenu={mainMenu} />
</AppContext.Provider>
</IntlProvider>
);
const StudioHeaderContext = ({ actionRowContent = null }) => {
const headerContextValue = useMemo(() => ({
authenticatedUser: {
userId: 'abc123',
username: 'edX',
roles: [],
administrator: false,
},
config: {
STUDIO_BASE_URL: process.env.STUDIO_BASE_URL,
SITE_NAME: process.env.SITE_NAME,
LOGIN_URL: process.env.LOGIN_URL,
LOGOUT_URL: process.env.LOGOUT_URL,
LOGO_URL: process.env.LOGO_URL,
},
}), []);
return (
<IntlProvider locale="en" messages={{}}>
<AppContext.Provider
value={headerContextValue}
>
<StudioHeader actionRowContent={actionRowContent} />
</AppContext.Provider>
</IntlProvider>
);
};
describe('<StudioHeader />', () => { describe('<StudioHeader />', () => {
it('renders correctly', () => { it('renders correctly', () => {
const component = ( const contextValue = {
<IntlProvider locale="en" messages={{}}> authenticatedUser: {
<AppContext.Provider userId: 'abc123',
value={{ username: 'edX',
authenticatedUser: { roles: [],
userId: 'abc123', administrator: false,
username: 'edX', },
roles: [], config: {
administrator: false, STUDIO_BASE_URL: process.env.STUDIO_BASE_URL,
}, SITE_NAME: process.env.SITE_NAME,
config: { LOGIN_URL: process.env.LOGIN_URL,
STUDIO_BASE_URL: process.env.STUDIO_BASE_URL, LOGOUT_URL: process.env.LOGOUT_URL,
SITE_NAME: process.env.SITE_NAME, LOGO_URL: process.env.LOGO_URL,
LOGIN_URL: process.env.LOGIN_URL, },
LOGOUT_URL: process.env.LOGOUT_URL,
LOGO_URL: process.env.LOGO_URL,
},
}}
>
<StudioHeader />
</AppContext.Provider>
</IntlProvider>
);
const wrapper = TestRenderer.create(component);
expect(wrapper.toJSON()).toMatchSnapshot();
});
it('renders correctly with the optional app menu', () => {
const appMenu = {
content: 'App Menu',
menuItems: [
{
type: 'dropdown',
href: 'https://menu-href-url.org',
content: 'Content 1',
},
{
type: 'dropdown',
href: 'https://menu-href-url.org',
content: 'Content 2',
},
{
type: 'dropdown',
href: 'https://menu-href-url.org',
content: 'Content 3',
},
],
}; };
const component = (
<IntlProvider locale="en" messages={{}}> const component = <StudioHeaderComponent contextValue={contextValue} />;
<AppContext.Provider
value={{
authenticatedUser: {
userId: 'abc123',
username: 'edX',
roles: [],
administrator: false,
},
config: {
STUDIO_BASE_URL: process.env.STUDIO_BASE_URL,
SITE_NAME: process.env.SITE_NAME,
LOGIN_URL: process.env.LOGIN_URL,
LOGOUT_URL: process.env.LOGOUT_URL,
LOGO_URL: process.env.LOGO_URL,
},
}}
>
<StudioHeader appMenu={appMenu} />
</AppContext.Provider>
</IntlProvider>
);
const wrapper = TestRenderer.create(component); const wrapper = TestRenderer.create(component);
expect(wrapper.toJSON()).toMatchSnapshot(); expect(wrapper.toJSON()).toMatchSnapshot();
}); });
it('renders correctly with the optional main menu', () => { it('renders correctly with optional action row content', () => {
const mainMenu = [ const actionRowContent = (
{ <>
type: 'dropdown', <Dropdown>
href: 'https://menu-href-url.org', <Dropdown.Toggle variant="outline-primary" id="library-header-menu-dropdown">
content: 'Content 1', Settings
}, </Dropdown.Toggle>
{ <Dropdown.Menu>
type: 'dropdown', <Dropdown.Item as={Link} to="#">Dropdown Item 1</Dropdown.Item>
href: 'https://menu-href-url.org', <Dropdown.Item as={Link} to="#">Dropdown Item 2</Dropdown.Item>
content: 'Content 2', <Dropdown.Item as={Link} to="#">Dropdown Item 3</Dropdown.Item>
}, </Dropdown.Menu>
{ </Dropdown>
type: 'dropdown', <ActionRow.Spacer />
href: 'https://menu-href-url.org', <Button
content: 'Content 3', variant="tertiary"
}, href="#"
]; rel="noopener noreferrer"
const component = ( target="_blank"
<IntlProvider locale="en" messages={{}}> title="Help Button"
<AppContext.Provider >Help
value={{ </Button>
authenticatedUser: { </>
userId: 'abc123',
username: 'edX',
roles: [],
administrator: false,
},
config: {
STUDIO_BASE_URL: process.env.STUDIO_BASE_URL,
SITE_NAME: process.env.SITE_NAME,
LOGIN_URL: process.env.LOGIN_URL,
LOGOUT_URL: process.env.LOGOUT_URL,
LOGO_URL: process.env.LOGO_URL,
},
}}
>
<StudioHeader mainMenu={mainMenu} />
</AppContext.Provider>
</IntlProvider>
); );
const component = <StudioHeaderContext actionRowContent={actionRowContent} />;
const wrapper = TestRenderer.create(component); const wrapper = TestRenderer.create(component);
expect(wrapper.toJSON()).toMatchSnapshot(); expect(wrapper.toJSON()).toMatchSnapshot();

View File

@@ -21,83 +21,83 @@ exports[`<StudioHeader /> renders correctly 1`] = `
className="logo" className="logo"
src="https://edx-cdn.org/v3/default/logo.svg" src="https://edx-cdn.org/v3/default/logo.svg"
/> />
<nav <div
aria-label="Main" className="pgn__action-row"
className="nav main-nav"
/>
<nav
aria-label="Secondary"
className="nav secondary-menu-container align-items-center ml-auto"
> >
<div <nav
className="menu null" aria-label="Secondary"
onKeyDown={[Function]} className="nav secondary-menu-container align-items-center ml-auto"
onMouseEnter={[Function]}
onMouseLeave={[Function]}
> >
<button <div
aria-expanded={false} className="menu null"
aria-haspopup="menu" onKeyDown={[Function]}
aria-label="Account menu for edX" onMouseEnter={[Function]}
className="menu-trigger btn btn-outline-primary d-inline-flex align-items-center pl-2 pr-3" onMouseLeave={[Function]}
onClick={[Function]}
> >
<span <button
className="avatar overflow-hidden d-inline-flex rounded-circle mr-2" aria-expanded={false}
style={ aria-haspopup="menu"
Object { aria-label="Account menu for edX"
"height": "1.5em", className="menu-trigger btn btn-outline-primary d-inline-flex align-items-center pl-2 pr-3"
"width": "1.5em", onClick={[Function]}
}
}
> >
<svg <span
aria-hidden={true} className="avatar overflow-hidden d-inline-flex rounded-circle mr-2"
focusable="false"
height="24px"
role="img"
style={ style={
Object { Object {
"height": "1.5em", "height": "1.5em",
"width": "1.5em", "width": "1.5em",
} }
} }
>
<svg
aria-hidden={true}
focusable="false"
height="24px"
role="img"
style={
Object {
"height": "1.5em",
"width": "1.5em",
}
}
version="1.1"
viewBox="0 0 24 24"
width="24px"
>
<path
d="M4.10255106,18.1351061 C4.7170266,16.0581859 8.01891846,14.4720277 12,14.4720277 C15.9810815,14.4720277 19.2829734,16.0581859 19.8974489,18.1351061 C21.215206,16.4412566 22,14.3122775 22,12 C22,6.4771525 17.5228475,2 12,2 C6.4771525,2 2,6.4771525 2,12 C2,14.3122775 2.78479405,16.4412566 4.10255106,18.1351061 Z M12,24 C5.372583,24 0,18.627417 0,12 C0,5.372583 5.372583,0 12,0 C18.627417,0 24,5.372583 24,12 C24,18.627417 18.627417,24 12,24 Z M12,13 C9.790861,13 8,11.209139 8,9 C8,6.790861 9.790861,5 12,5 C14.209139,5 16,6.790861 16,9 C16,11.209139 14.209139,13 12,13 Z"
fill="currentColor"
/>
</svg>
</span>
edX
<svg
aria-hidden={true}
focusable="false"
height="16px"
role="img"
version="1.1" version="1.1"
viewBox="0 0 24 24" viewBox="0 0 16 16"
width="24px" width="16px"
> >
<path <path
d="M4.10255106,18.1351061 C4.7170266,16.0581859 8.01891846,14.4720277 12,14.4720277 C15.9810815,14.4720277 19.2829734,16.0581859 19.8974489,18.1351061 C21.215206,16.4412566 22,14.3122775 22,12 C22,6.4771525 17.5228475,2 12,2 C6.4771525,2 2,6.4771525 2,12 C2,14.3122775 2.78479405,16.4412566 4.10255106,18.1351061 Z M12,24 C5.372583,24 0,18.627417 0,12 C0,5.372583 5.372583,0 12,0 C18.627417,0 24,5.372583 24,12 C24,18.627417 18.627417,24 12,24 Z M12,13 C9.790861,13 8,11.209139 8,9 C8,6.790861 9.790861,5 12,5 C14.209139,5 16,6.790861 16,9 C16,11.209139 14.209139,13 12,13 Z" d="M7,4 L7,8 L11,8 L11,10 L5,10 L5,4 L7,4 Z"
fill="currentColor" fill="currentColor"
transform="translate(8.000000, 7.000000) rotate(-45.000000) translate(-8.000000, -7.000000) "
/> />
</svg> </svg>
</span> </button>
edX </div>
</nav>
<svg </div>
aria-hidden={true}
focusable="false"
height="16px"
role="img"
version="1.1"
viewBox="0 0 16 16"
width="16px"
>
<path
d="M7,4 L7,8 L11,8 L11,10 L5,10 L5,4 L7,4 Z"
fill="currentColor"
transform="translate(8.000000, 7.000000) rotate(-45.000000) translate(-8.000000, -7.000000) "
/>
</svg>
</button>
</div>
</nav>
</div> </div>
</div> </div>
</header> </header>
`; `;
exports[`<StudioHeader /> renders correctly with the optional app menu 1`] = ` exports[`<StudioHeader /> renders correctly with optional action row content 1`] = `
<header <header
className="site-header-desktop" className="site-header-desktop"
> >
@@ -118,307 +118,108 @@ exports[`<StudioHeader /> renders correctly with the optional app menu 1`] = `
className="logo" className="logo"
src="https://edx-cdn.org/v3/default/logo.svg" src="https://edx-cdn.org/v3/default/logo.svg"
/> />
<nav <div
aria-label="Main" className="pgn__action-row"
className="nav main-nav"
/>
<nav
aria-label="App"
className="nav app-nav"
> >
<div <div
className="menu null" className="pgn__dropdown pgn__dropdown-light dropdown"
onKeyDown={[Function]} data-testid="dropdown"
onMouseEnter={[Function]}
onMouseLeave={[Function]}
>
<a
aria-expanded={false}
aria-haspopup="menu"
className="menu-trigger nav-link d-inline-flex align-items-center"
onClick={[Function]}
>
App Menu
<svg
aria-hidden={true}
focusable="false"
height="16px"
role="img"
version="1.1"
viewBox="0 0 16 16"
width="16px"
>
<path
d="M7,4 L7,8 L11,8 L11,10 L5,10 L5,4 L7,4 Z"
fill="currentColor"
transform="translate(8.000000, 7.000000) rotate(-45.000000) translate(-8.000000, -7.000000) "
/>
</svg>
</a>
</div>
</nav>
<nav
aria-label="Secondary"
className="nav secondary-menu-container align-items-center ml-auto"
>
<div
className="menu null"
onKeyDown={[Function]}
onMouseEnter={[Function]}
onMouseLeave={[Function]}
> >
<button <button
aria-expanded={false} aria-expanded={false}
aria-haspopup="menu" aria-haspopup={true}
aria-label="Account menu for edX" className="dropdown-toggle btn btn-outline-primary"
className="menu-trigger btn btn-outline-primary d-inline-flex align-items-center pl-2 pr-3" disabled={false}
id="library-header-menu-dropdown"
onClick={[Function]} onClick={[Function]}
type="button"
> >
<span Settings
className="avatar overflow-hidden d-inline-flex rounded-circle mr-2" </button>
style={ </div>
Object { <span
"height": "1.5em", className="pgn__action-row-spacer"
"width": "1.5em", />
} <a
} className="btn btn-tertiary"
href="#"
onClick={[Function]}
onKeyDown={[Function]}
rel="noopener noreferrer"
role="button"
target="_blank"
title="Help Button"
>
Help
</a>
<nav
aria-label="Secondary"
className="nav secondary-menu-container align-items-center ml-auto"
>
<div
className="menu null"
onKeyDown={[Function]}
onMouseEnter={[Function]}
onMouseLeave={[Function]}
>
<button
aria-expanded={false}
aria-haspopup="menu"
aria-label="Account menu for edX"
className="menu-trigger btn btn-outline-primary d-inline-flex align-items-center pl-2 pr-3"
onClick={[Function]}
> >
<svg <span
aria-hidden={true} className="avatar overflow-hidden d-inline-flex rounded-circle mr-2"
focusable="false"
height="24px"
role="img"
style={ style={
Object { Object {
"height": "1.5em", "height": "1.5em",
"width": "1.5em", "width": "1.5em",
} }
} }
version="1.1"
viewBox="0 0 24 24"
width="24px"
> >
<path <svg
d="M4.10255106,18.1351061 C4.7170266,16.0581859 8.01891846,14.4720277 12,14.4720277 C15.9810815,14.4720277 19.2829734,16.0581859 19.8974489,18.1351061 C21.215206,16.4412566 22,14.3122775 22,12 C22,6.4771525 17.5228475,2 12,2 C6.4771525,2 2,6.4771525 2,12 C2,14.3122775 2.78479405,16.4412566 4.10255106,18.1351061 Z M12,24 C5.372583,24 0,18.627417 0,12 C0,5.372583 5.372583,0 12,0 C18.627417,0 24,5.372583 24,12 C24,18.627417 18.627417,24 12,24 Z M12,13 C9.790861,13 8,11.209139 8,9 C8,6.790861 9.790861,5 12,5 C14.209139,5 16,6.790861 16,9 C16,11.209139 14.209139,13 12,13 Z" aria-hidden={true}
fill="currentColor" focusable="false"
/> height="24px"
</svg> role="img"
</span> style={
edX Object {
"height": "1.5em",
<svg "width": "1.5em",
aria-hidden={true} }
focusable="false" }
height="16px" version="1.1"
role="img" viewBox="0 0 24 24"
version="1.1" width="24px"
viewBox="0 0 16 16" >
width="16px" <path
> d="M4.10255106,18.1351061 C4.7170266,16.0581859 8.01891846,14.4720277 12,14.4720277 C15.9810815,14.4720277 19.2829734,16.0581859 19.8974489,18.1351061 C21.215206,16.4412566 22,14.3122775 22,12 C22,6.4771525 17.5228475,2 12,2 C6.4771525,2 2,6.4771525 2,12 C2,14.3122775 2.78479405,16.4412566 4.10255106,18.1351061 Z M12,24 C5.372583,24 0,18.627417 0,12 C0,5.372583 5.372583,0 12,0 C18.627417,0 24,5.372583 24,12 C24,18.627417 18.627417,24 12,24 Z M12,13 C9.790861,13 8,11.209139 8,9 C8,6.790861 9.790861,5 12,5 C14.209139,5 16,6.790861 16,9 C16,11.209139 14.209139,13 12,13 Z"
<path fill="currentColor"
d="M7,4 L7,8 L11,8 L11,10 L5,10 L5,4 L7,4 Z" />
fill="currentColor" </svg>
transform="translate(8.000000, 7.000000) rotate(-45.000000) translate(-8.000000, -7.000000) " </span>
/> edX
</svg>
</button>
</div>
</nav>
</div>
</div>
</header>
`;
exports[`<StudioHeader /> renders correctly with the optional main menu 1`] = `
<header
className="site-header-desktop"
>
<a
className="nav-skip sr-only sr-only-focusable"
href="#main"
>
Skip to main content
</a>
<div
className="container-fluid null"
>
<div
className="nav-container position-relative d-flex align-items-center"
>
<img
alt="edX"
className="logo"
src="https://edx-cdn.org/v3/default/logo.svg"
/>
<nav
aria-label="Main"
className="nav main-nav"
>
<div
className="menu nav-item"
onKeyDown={[Function]}
onMouseEnter={[Function]}
onMouseLeave={[Function]}
>
<a
aria-expanded={false}
aria-haspopup="menu"
className="menu-trigger nav-link d-inline-flex align-items-center"
href="https://menu-href-url.org"
onClick={[Function]}
>
Content 1
<svg
aria-hidden={true}
focusable="false"
height="16px"
role="img"
version="1.1"
viewBox="0 0 16 16"
width="16px"
>
<path
d="M7,4 L7,8 L11,8 L11,10 L5,10 L5,4 L7,4 Z"
fill="currentColor"
transform="translate(8.000000, 7.000000) rotate(-45.000000) translate(-8.000000, -7.000000) "
/>
</svg>
</a>
</div>
<div
className="menu nav-item"
onKeyDown={[Function]}
onMouseEnter={[Function]}
onMouseLeave={[Function]}
>
<a
aria-expanded={false}
aria-haspopup="menu"
className="menu-trigger nav-link d-inline-flex align-items-center"
href="https://menu-href-url.org"
onClick={[Function]}
>
Content 2
<svg
aria-hidden={true}
focusable="false"
height="16px"
role="img"
version="1.1"
viewBox="0 0 16 16"
width="16px"
>
<path
d="M7,4 L7,8 L11,8 L11,10 L5,10 L5,4 L7,4 Z"
fill="currentColor"
transform="translate(8.000000, 7.000000) rotate(-45.000000) translate(-8.000000, -7.000000) "
/>
</svg>
</a>
</div>
<div
className="menu nav-item"
onKeyDown={[Function]}
onMouseEnter={[Function]}
onMouseLeave={[Function]}
>
<a
aria-expanded={false}
aria-haspopup="menu"
className="menu-trigger nav-link d-inline-flex align-items-center"
href="https://menu-href-url.org"
onClick={[Function]}
>
Content 3
<svg
aria-hidden={true}
focusable="false"
height="16px"
role="img"
version="1.1"
viewBox="0 0 16 16"
width="16px"
>
<path
d="M7,4 L7,8 L11,8 L11,10 L5,10 L5,4 L7,4 Z"
fill="currentColor"
transform="translate(8.000000, 7.000000) rotate(-45.000000) translate(-8.000000, -7.000000) "
/>
</svg>
</a>
</div>
</nav>
<nav
aria-label="Secondary"
className="nav secondary-menu-container align-items-center ml-auto"
>
<div
className="menu null"
onKeyDown={[Function]}
onMouseEnter={[Function]}
onMouseLeave={[Function]}
>
<button
aria-expanded={false}
aria-haspopup="menu"
aria-label="Account menu for edX"
className="menu-trigger btn btn-outline-primary d-inline-flex align-items-center pl-2 pr-3"
onClick={[Function]}
>
<span
className="avatar overflow-hidden d-inline-flex rounded-circle mr-2"
style={
Object {
"height": "1.5em",
"width": "1.5em",
}
}
>
<svg <svg
aria-hidden={true} aria-hidden={true}
focusable="false" focusable="false"
height="24px" height="16px"
role="img" role="img"
style={
Object {
"height": "1.5em",
"width": "1.5em",
}
}
version="1.1" version="1.1"
viewBox="0 0 24 24" viewBox="0 0 16 16"
width="24px" width="16px"
> >
<path <path
d="M4.10255106,18.1351061 C4.7170266,16.0581859 8.01891846,14.4720277 12,14.4720277 C15.9810815,14.4720277 19.2829734,16.0581859 19.8974489,18.1351061 C21.215206,16.4412566 22,14.3122775 22,12 C22,6.4771525 17.5228475,2 12,2 C6.4771525,2 2,6.4771525 2,12 C2,14.3122775 2.78479405,16.4412566 4.10255106,18.1351061 Z M12,24 C5.372583,24 0,18.627417 0,12 C0,5.372583 5.372583,0 12,0 C18.627417,0 24,5.372583 24,12 C24,18.627417 18.627417,24 12,24 Z M12,13 C9.790861,13 8,11.209139 8,9 C8,6.790861 9.790861,5 12,5 C14.209139,5 16,6.790861 16,9 C16,11.209139 14.209139,13 12,13 Z" d="M7,4 L7,8 L11,8 L11,10 L5,10 L5,4 L7,4 Z"
fill="currentColor" fill="currentColor"
transform="translate(8.000000, 7.000000) rotate(-45.000000) translate(-8.000000, -7.000000) "
/> />
</svg> </svg>
</span> </button>
edX </div>
</nav>
<svg </div>
aria-hidden={true}
focusable="false"
height="16px"
role="img"
version="1.1"
viewBox="0 0 16 16"
width="16px"
>
<path
d="M7,4 L7,8 L11,8 L11,10 L5,10 L5,4 L7,4 Z"
fill="currentColor"
transform="translate(8.000000, 7.000000) rotate(-45.000000) translate(-8.000000, -7.000000) "
/>
</svg>
</button>
</div>
</nav>
</div> </div>
</div> </div>
</header> </header>

View File

@@ -1,34 +1,28 @@
import arMessages from './messages/ar.json'; import arMessages from './messages/ar.json';
import caMessages from './messages/ca.json';
import heMessages from './messages/he.json';
import idMessages from './messages/id.json';
import plMessages from './messages/pl.json';
import ruMessages from './messages/ru.json';
import thMessages from './messages/th.json';
import ukMessages from './messages/uk.json';
// no need to import en messages-- they are in the defaultMessage field
import es419Messages from './messages/es_419.json';
import frMessages from './messages/fr.json'; import frMessages from './messages/fr.json';
import kokrMessages from './messages/ko_KR.json'; import es419Messages from './messages/es_419.json';
import ptbrMessages from './messages/pt_BR.json';
import zhcnMessages from './messages/zh_CN.json'; import zhcnMessages from './messages/zh_CN.json';
import ptMessages from './messages/pt.json';
import itMessages from './messages/it.json';
import ukMessages from './messages/uk.json';
import deMessages from './messages/de.json';
import ruMessages from './messages/ru.json';
import hiMessages from './messages/hi.json';
import frCAMessages from './messages/fr_CA.json';
// no need to import en messages-- they are in the defaultMessage field
const messages = { const messages = {
ar: arMessages, ar: arMessages,
ca: caMessages,
he: heMessages,
id: idMessages,
pl: plMessages,
ru: ruMessages,
th: thMessages,
uk: ukMessages,
'es-419': es419Messages, 'es-419': es419Messages,
fr: frMessages, fr: frMessages,
'zh-cn': zhcnMessages, 'zh-cn': zhcnMessages,
'ko-kr': kokrMessages, pt: ptMessages,
'pt-br': ptbrMessages, it: itMessages,
de: deMessages,
hi: hiMessages,
'fr-ca': frCAMessages,
ru: ruMessages,
uk: ukMessages,
}; };
export default messages; export default messages;

View File

@@ -1 +0,0 @@
{}

33
src/i18n/messages/de.json Normal file
View File

@@ -0,0 +1,33 @@
{
"general.register.sentenceCase": "Register",
"general.signIn.sentenceCase": "Sign in",
"header.links.courses": "Courses",
"header.links.programs": "Programs",
"header.links.content.search": "Discover New",
"header.links.schools": "Schools & Partners",
"header.user.menu.dashboard": "Dashboard",
"header.user.menu.profile": "Profile",
"header.user.menu.account.settings": "Account",
"header.user.menu.order.history": "Order History",
"header.user.menu.logout": "Logout",
"header.user.menu.login": "Login",
"header.user.menu.register": "Sign Up",
"header.user.menu.studio.home": "Studio Home",
"header.user.menu.studio.maintenance": "Maintenance",
"header.label.account.nav": "Account",
"header.label.account.menu": "Account Menu",
"header.label.account.menu.for": "Account menu for {username}",
"header.label.main.nav": "Main",
"header.label.main.menu": "Main Menu",
"header.label.main.header": "Main",
"header.label.secondary.nav": "Secondary",
"header.label.skip.nav": "Skip to main content",
"header.label.app.nav": "App",
"header.menu.dashboard.label": "Dashboard",
"header.help.label": "Help",
"header.menu.profile.label": "Profile",
"header.menu.account.label": "Account",
"header.menu.orderHistory.label": "Order History",
"header.navigation.skipNavLink": "Skip to main content.",
"header.menu.signOut.label": "Sign Out"
}

View File

@@ -1 +0,0 @@
{}

33
src/i18n/messages/hi.json Normal file
View File

@@ -0,0 +1,33 @@
{
"general.register.sentenceCase": "Register",
"general.signIn.sentenceCase": "Sign in",
"header.links.courses": "Courses",
"header.links.programs": "Programs",
"header.links.content.search": "Discover New",
"header.links.schools": "Schools & Partners",
"header.user.menu.dashboard": "Dashboard",
"header.user.menu.profile": "Profile",
"header.user.menu.account.settings": "Account",
"header.user.menu.order.history": "Order History",
"header.user.menu.logout": "Logout",
"header.user.menu.login": "Login",
"header.user.menu.register": "Sign Up",
"header.user.menu.studio.home": "Studio Home",
"header.user.menu.studio.maintenance": "Maintenance",
"header.label.account.nav": "Account",
"header.label.account.menu": "Account Menu",
"header.label.account.menu.for": "Account menu for {username}",
"header.label.main.nav": "Main",
"header.label.main.menu": "Main Menu",
"header.label.main.header": "Main",
"header.label.secondary.nav": "Secondary",
"header.label.skip.nav": "Skip to main content",
"header.label.app.nav": "App",
"header.menu.dashboard.label": "Dashboard",
"header.help.label": "Help",
"header.menu.profile.label": "Profile",
"header.menu.account.label": "Account",
"header.menu.orderHistory.label": "Order History",
"header.navigation.skipNavLink": "Skip to main content.",
"header.menu.signOut.label": "Sign Out"
}

View File

@@ -1 +0,0 @@
{}

33
src/i18n/messages/it.json Normal file
View File

@@ -0,0 +1,33 @@
{
"general.register.sentenceCase": "Register",
"general.signIn.sentenceCase": "Sign in",
"header.links.courses": "Courses",
"header.links.programs": "Programs",
"header.links.content.search": "Discover New",
"header.links.schools": "Schools & Partners",
"header.user.menu.dashboard": "Dashboard",
"header.user.menu.profile": "Profile",
"header.user.menu.account.settings": "Account",
"header.user.menu.order.history": "Order History",
"header.user.menu.logout": "Logout",
"header.user.menu.login": "Login",
"header.user.menu.register": "Sign Up",
"header.user.menu.studio.home": "Studio Home",
"header.user.menu.studio.maintenance": "Maintenance",
"header.label.account.nav": "Account",
"header.label.account.menu": "Account Menu",
"header.label.account.menu.for": "Account menu for {username}",
"header.label.main.nav": "Main",
"header.label.main.menu": "Main Menu",
"header.label.main.header": "Main",
"header.label.secondary.nav": "Secondary",
"header.label.skip.nav": "Skip to main content",
"header.label.app.nav": "App",
"header.menu.dashboard.label": "Dashboard",
"header.help.label": "Help",
"header.menu.profile.label": "Profile",
"header.menu.account.label": "Account",
"header.menu.orderHistory.label": "Order History",
"header.navigation.skipNavLink": "Skip to main content.",
"header.menu.signOut.label": "Sign Out"
}

View File

@@ -1,2 +0,0 @@
{
}

View File

@@ -1 +0,0 @@
{}

33
src/i18n/messages/pt.json Normal file
View File

@@ -0,0 +1,33 @@
{
"general.register.sentenceCase": "Register",
"general.signIn.sentenceCase": "Sign in",
"header.links.courses": "Courses",
"header.links.programs": "Programs",
"header.links.content.search": "Discover New",
"header.links.schools": "Schools & Partners",
"header.user.menu.dashboard": "Dashboard",
"header.user.menu.profile": "Profile",
"header.user.menu.account.settings": "Account",
"header.user.menu.order.history": "Order History",
"header.user.menu.logout": "Logout",
"header.user.menu.login": "Login",
"header.user.menu.register": "Sign Up",
"header.user.menu.studio.home": "Studio Home",
"header.user.menu.studio.maintenance": "Maintenance",
"header.label.account.nav": "Account",
"header.label.account.menu": "Account Menu",
"header.label.account.menu.for": "Account menu for {username}",
"header.label.main.nav": "Main",
"header.label.main.menu": "Main Menu",
"header.label.main.header": "Main",
"header.label.secondary.nav": "Secondary",
"header.label.skip.nav": "Skip to main content",
"header.label.app.nav": "App",
"header.menu.dashboard.label": "Dashboard",
"header.help.label": "Help",
"header.menu.profile.label": "Profile",
"header.menu.account.label": "Account",
"header.menu.orderHistory.label": "Order History",
"header.navigation.skipNavLink": "Skip to main content.",
"header.menu.signOut.label": "Sign Out"
}

View File

@@ -1,2 +0,0 @@
{
}

View File

@@ -1 +1,33 @@
{} {
"general.register.sentenceCase": "Register",
"general.signIn.sentenceCase": "Sign in",
"header.links.courses": "Courses",
"header.links.programs": "Programs",
"header.links.content.search": "Discover New",
"header.links.schools": "Schools & Partners",
"header.user.menu.dashboard": "Dashboard",
"header.user.menu.profile": "Profile",
"header.user.menu.account.settings": "Account",
"header.user.menu.order.history": "Order History",
"header.user.menu.logout": "Logout",
"header.user.menu.login": "Login",
"header.user.menu.register": "Sign Up",
"header.user.menu.studio.home": "Studio Home",
"header.user.menu.studio.maintenance": "Maintenance",
"header.label.account.nav": "Account",
"header.label.account.menu": "Account Menu",
"header.label.account.menu.for": "Account menu for {username}",
"header.label.main.nav": "Main",
"header.label.main.menu": "Main Menu",
"header.label.main.header": "Main",
"header.label.secondary.nav": "Secondary",
"header.label.skip.nav": "Skip to main content",
"header.label.app.nav": "App",
"header.menu.dashboard.label": "Dashboard",
"header.help.label": "Help",
"header.menu.profile.label": "Profile",
"header.menu.account.label": "Account",
"header.menu.orderHistory.label": "Order History",
"header.navigation.skipNavLink": "Skip to main content.",
"header.menu.signOut.label": "Sign Out"
}

View File

@@ -1 +0,0 @@
{}

View File

@@ -1 +1,33 @@
{} {
"general.register.sentenceCase": "Register",
"general.signIn.sentenceCase": "Sign in",
"header.links.courses": "Courses",
"header.links.programs": "Programs",
"header.links.content.search": "Discover New",
"header.links.schools": "Schools & Partners",
"header.user.menu.dashboard": "Dashboard",
"header.user.menu.profile": "Profile",
"header.user.menu.account.settings": "Account",
"header.user.menu.order.history": "Order History",
"header.user.menu.logout": "Logout",
"header.user.menu.login": "Login",
"header.user.menu.register": "Sign Up",
"header.user.menu.studio.home": "Studio Home",
"header.user.menu.studio.maintenance": "Maintenance",
"header.label.account.nav": "Account",
"header.label.account.menu": "Account Menu",
"header.label.account.menu.for": "Account menu for {username}",
"header.label.main.nav": "Main",
"header.label.main.menu": "Main Menu",
"header.label.main.header": "Main",
"header.label.secondary.nav": "Secondary",
"header.label.skip.nav": "Skip to main content",
"header.label.app.nav": "App",
"header.menu.dashboard.label": "Dashboard",
"header.help.label": "Help",
"header.menu.profile.label": "Profile",
"header.menu.account.label": "Account",
"header.menu.orderHistory.label": "Order History",
"header.navigation.skipNavLink": "Skip to main content.",
"header.menu.signOut.label": "Sign Out"
}

View File

@@ -7,25 +7,23 @@ import { Button } from '@edx/paragon';
import genericMessages from '../generic/messages'; import genericMessages from '../generic/messages';
function AnonymousUserMenu({ intl }) { const AnonymousUserMenu = ({ intl }) => (
return ( <div>
<div> <Button
<Button className="mr-3"
className="mr-3" variant="outline-primary"
variant="outline-primary" href={`${getConfig().LMS_BASE_URL}/register?next=${encodeURIComponent(global.location.href)}`}
href={`${getConfig().LMS_BASE_URL}/register?next=${encodeURIComponent(global.location.href)}`} >
> {intl.formatMessage(genericMessages.registerSentenceCase)}
{intl.formatMessage(genericMessages.registerSentenceCase)} </Button>
</Button> <Button
<Button variant="primary"
variant="primary" href={`${getLoginRedirectUrl(global.location.href)}`}
href={`${getLoginRedirectUrl(global.location.href)}`} >
> {intl.formatMessage(genericMessages.signInSentenceCase)}
{intl.formatMessage(genericMessages.signInSentenceCase)} </Button>
</Button> </div>
</div> );
);
}
AnonymousUserMenu.propTypes = { AnonymousUserMenu.propTypes = {
intl: intlShape.isRequired, intl: intlShape.isRequired,

View File

@@ -10,7 +10,7 @@ import { Dropdown } from '@edx/paragon';
import messages from './messages'; import messages from './messages';
function AuthenticatedUserDropdown({ intl, username }) { const AuthenticatedUserDropdown = ({ intl, username }) => {
const dashboardMenuItem = ( const dashboardMenuItem = (
<Dropdown.Item href={`${getConfig().LMS_BASE_URL}/dashboard`}> <Dropdown.Item href={`${getConfig().LMS_BASE_URL}/dashboard`}>
{intl.formatMessage(messages.dashboard)} {intl.formatMessage(messages.dashboard)}
@@ -29,10 +29,10 @@ function AuthenticatedUserDropdown({ intl, username }) {
</Dropdown.Toggle> </Dropdown.Toggle>
<Dropdown.Menu className="dropdown-menu-right"> <Dropdown.Menu className="dropdown-menu-right">
{dashboardMenuItem} {dashboardMenuItem}
<Dropdown.Item href={`${getConfig().LMS_BASE_URL}/u/${username}`}> <Dropdown.Item href={`${getConfig().ACCOUNT_PROFILE_URL}/u/${username}`}>
{intl.formatMessage(messages.profile)} {intl.formatMessage(messages.profile)}
</Dropdown.Item> </Dropdown.Item>
<Dropdown.Item href={`${getConfig().LMS_BASE_URL}/account/settings`}> <Dropdown.Item href={getConfig().ACCOUNT_SETTINGS_URL}>
{intl.formatMessage(messages.account)} {intl.formatMessage(messages.account)}
</Dropdown.Item> </Dropdown.Item>
{ getConfig().ORDER_HISTORY_URL && ( { getConfig().ORDER_HISTORY_URL && (
@@ -47,7 +47,7 @@ function AuthenticatedUserDropdown({ intl, username }) {
</Dropdown> </Dropdown>
</> </>
); );
} };
AuthenticatedUserDropdown.propTypes = { AuthenticatedUserDropdown.propTypes = {
intl: intlShape.isRequired, intl: intlShape.isRequired,

View File

@@ -8,18 +8,16 @@ import AnonymousUserMenu from './AnonymousUserMenu';
import AuthenticatedUserDropdown from './AuthenticatedUserDropdown'; import AuthenticatedUserDropdown from './AuthenticatedUserDropdown';
import messages from './messages'; import messages from './messages';
function LinkedLogo({ const LinkedLogo = ({
href, href,
src, src,
alt, alt,
...attributes ...attributes
}) { }) => (
return ( <a href={href} {...attributes}>
<a href={href} {...attributes}> <img className="d-block" src={src} alt={alt} />
<img className="d-block" src={src} alt={alt} /> </a>
</a> );
);
}
LinkedLogo.propTypes = { LinkedLogo.propTypes = {
href: PropTypes.string.isRequired, href: PropTypes.string.isRequired,
@@ -27,9 +25,9 @@ LinkedLogo.propTypes = {
alt: PropTypes.string.isRequired, alt: PropTypes.string.isRequired,
}; };
function LearningHeader({ const LearningHeader = ({
courseOrg, courseNumber, courseTitle, intl, showUserDropdown, courseOrg, courseNumber, courseTitle, intl, showUserDropdown,
}) { }) => {
const { authenticatedUser } = useContext(AppContext); const { authenticatedUser } = useContext(AppContext);
const headerLogo = ( const headerLogo = (
@@ -61,7 +59,7 @@ function LearningHeader({
</div> </div>
</header> </header>
); );
} };
LearningHeader.propTypes = { LearningHeader.propTypes = {
courseOrg: PropTypes.string, courseOrg: PropTypes.string,

View File

@@ -22,6 +22,8 @@ Enzyme.configure({ adapter: new Adapter() });
// These configuration values are usually set in webpack's EnvironmentPlugin however // These configuration values are usually set in webpack's EnvironmentPlugin however
// Jest does not use webpack so we need to set these so for testing // Jest does not use webpack so we need to set these so for testing
process.env.ACCESS_TOKEN_COOKIE_NAME = 'edx-jwt-cookie-header-payload'; process.env.ACCESS_TOKEN_COOKIE_NAME = 'edx-jwt-cookie-header-payload';
process.env.ACCOUNT_PROFILE_URL = 'http://localhost:1995';
process.env.ACCOUNT_SETTINGS_URL = 'http://localhost:1997';
process.env.BASE_URL = 'localhost:1995'; process.env.BASE_URL = 'localhost:1995';
process.env.CREDENTIALS_BASE_URL = 'http://localhost:18150'; process.env.CREDENTIALS_BASE_URL = 'http://localhost:18150';
process.env.CSRF_TOKEN_API_PATH = '/csrf/api/v1/token'; process.env.CSRF_TOKEN_API_PATH = '/csrf/api/v1/token';
@@ -102,16 +104,14 @@ function render(
...renderOptions ...renderOptions
} = {}, } = {},
) { ) {
function Wrapper({ children }) { const Wrapper = ({ children }) => (
return (
// eslint-disable-next-line react/jsx-filename-extension // eslint-disable-next-line react/jsx-filename-extension
<IntlProvider locale="en"> <IntlProvider locale="en">
<AppProvider store={store}> <AppProvider store={store}>
{children} {children}
</AppProvider> </AppProvider>
</IntlProvider> </IntlProvider>
); );
}
Wrapper.propTypes = { Wrapper.propTypes = {
children: PropTypes.node.isRequired, children: PropTypes.node.isRequired,