Compare commits

..

2 Commits

Author SHA1 Message Date
Jawayria
916848577e fix: update release workflow 2022-05-13 18:21:44 +05:00
Jawayria
eb1eb84f56 chore!: Dropped support for Node 12 2022-05-13 18:20:27 +05:00
62 changed files with 32043 additions and 10425 deletions

View File

@@ -1,6 +1,4 @@
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,4 +1,3 @@
// 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

@@ -1,20 +0,0 @@
# This workflow runs when a comment is made on the ticket
# If the comment starts with "label: " it tries to apply
# the label indicated in rest of comment.
# If the comment starts with "remove label: ", it tries
# to remove the indicated label.
# Note: Labels are allowed to have spaces and this script does
# not parse spaces (as often a space is legitimate), so the command
# "label: really long lots of words label" will apply the
# label "really long lots of words label"
name: Allows for the adding and removing of labels via comment
on:
issue_comment:
types: [created]
jobs:
add_remove_labels:
uses: openedx/.github/.github/workflows/add-remove-label-on-comment.yml@master

View File

@@ -2,24 +2,25 @@ name: Default CI
on: on:
push: push:
branches: branches:
- master - 'master'
pull_request: pull_request:
branches: branches:
- '**' - '**'
jobs: jobs:
tests: tests:
runs-on: ubuntu-latest runs-on: ubuntu-latest
strategy:
matrix:
node: [16]
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v3 uses: actions/checkout@v2
with: with:
fetch-depth: 0 fetch-depth: 0
- name: Setup Nodejs Env
run: echo "NODE_VER=`cat .nvmrc`" >> $GITHUB_ENV
- name: Setup Nodejs - name: Setup Nodejs
uses: actions/setup-node@v3 uses: actions/setup-node@v2
with: with:
node-version: ${{ env.NODE_VER }} node-version: ${{ matrix.node }}
- name: Install dependencies - name: Install dependencies
run: npm ci run: npm ci
- name: Validate package-lock.json changes - name: Validate package-lock.json changes
@@ -28,9 +29,7 @@ jobs:
run: npm run lint run: npm run lint
- name: Test - name: Test
run: npm run test run: npm run test
- name: Build
run: npm run build
- name: i18n_extract - name: i18n_extract
run: npm run i18n_extract run: npm run i18n_extract
- name: Coverage - name: Coverage
uses: codecov/codecov-action@v3 uses: codecov/codecov-action@v2

View File

@@ -7,4 +7,4 @@ on:
jobs: jobs:
commitlint: commitlint:
uses: openedx/.github/.github/workflows/commitlint.yml@master uses: edx/.github/.github/workflows/commitlint.yml@master

View File

@@ -10,4 +10,5 @@ on:
jobs: jobs:
version-check: version-check:
uses: openedx/.github/.github/workflows/lockfile-check.yml@master uses: openedx/.github/.github/workflows/lockfileversion-check.yml@master

View File

@@ -2,40 +2,38 @@ name: Release CI
on: on:
push: push:
branches: branches:
- master - master
jobs: jobs:
release: release:
name: Release name: Release
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v3 uses: actions/checkout@v2
with: with:
fetch-depth: 0 fetch-depth: 0
- name: Setup Nodejs Env - name: Setup Node.js
run: echo "NODE_VER=`cat .nvmrc`" >> $GITHUB_ENV uses: actions/setup-node@v2
- name: Setup Node.js with:
uses: actions/setup-node@v3 node-version: 16
with: - name: Install dependencies
node-version: ${{ env.NODE_VER }} run: npm ci
- name: Install dependencies - name: Validate package-lock.json changes
run: npm ci run: make validate-no-uncommitted-package-lock-changes
- name: Validate package-lock.json changes - name: Lint
run: make validate-no-uncommitted-package-lock-changes run: npm run lint
- name: Lint - name: Test
run: npm run lint run: npm run test
- name: Test - name: i18n_extract
run: npm run test run: npm run i18n_extract
- name: i18n_extract - name: Coverage
run: npm run i18n_extract uses: codecov/codecov-action@v2
- name: Coverage - name: Build
uses: codecov/codecov-action@v3 run: npm run build
- name: Build - name: Release
run: npm run build uses: cycjimmy/semantic-release-action@v2
- name: Release with:
uses: cycjimmy/semantic-release-action@v3 semantic_version: 16
with: env:
semantic_version: 16 GITHUB_TOKEN: ${{ secrets.SEMANTIC_RELEASE_GITHUB_TOKEN }}
env: NPM_TOKEN: ${{ secrets.SEMANTIC_RELEASE_NPM_TOKEN }}
GITHUB_TOKEN: ${{ secrets.SEMANTIC_RELEASE_GITHUB_TOKEN }}
NPM_TOKEN: ${{ secrets.SEMANTIC_RELEASE_NPM_TOKEN }}

View File

@@ -1,12 +0,0 @@
# This workflow runs when a comment is made on the ticket
# If the comment starts with "assign me" it assigns the author to the
# ticket (case insensitive)
name: Assign comment author to ticket if they say "assign me"
on:
issue_comment:
types: [created]
jobs:
self_assign_by_comment:
uses: openedx/.github/.github/workflows/self-assign-issue.yml@master

2
.gitignore vendored
View File

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

1
.nvmrc
View File

@@ -1 +0,0 @@
18

View File

@@ -1,9 +1,11 @@
export TRANSIFEX_RESOURCE = frontend-component-header transifex_resource = frontend-component-header
transifex_langs = "ar,fr,es_419,zh_CN,pt,it,de,uk,ru,hi,fr_CA" transifex_langs = "ar,fr,fr_CA,es_419,zh_CN"
transifex_utils = ./node_modules/.bin/transifex-utils.js transifex_utils = ./node_modules/.bin/transifex-utils.js
i18n = ./src/i18n i18n = ./src/i18n
transifex_input = $(i18n)/transifex_input.json transifex_input = $(i18n)/transifex_input.json
tx_url1 = https://www.transifex.com/api/2/project/edx-platform/resource/$(transifex_resource)/translation/en/strings/
tx_url2 = https://www.transifex.com/api/2/project/edx-platform/resource/$(transifex_resource)/source/
# This directory must match .babelrc . # This directory must match .babelrc .
transifex_temp = ./temp/babel-plugin-react-intl transifex_temp = ./temp/babel-plugin-react-intl
@@ -40,15 +42,15 @@ push_translations:
# Pushing strings to Transifex... # Pushing strings to Transifex...
tx push -s tx push -s
# Fetching hashes from Transifex... # Fetching hashes from Transifex...
./node_modules/@edx/reactifex/bash_scripts/get_hashed_strings_v3.sh ./node_modules/reactifex/bash_scripts/get_hashed_strings.sh $(tx_url1)
# Writing out comments to file... # Writing out comments to file...
$(transifex_utils) $(transifex_temp) --comments --v3-scripts-path $(transifex_utils) $(transifex_temp) --comments
# Pushing comments to Transifex... # Pushing comments to Transifex...
./node_modules/@edx/reactifex/bash_scripts/put_comments_v3.sh ./node_modules/reactifex/bash_scripts/put_comments.sh $(tx_url2)
# Pulls translations from Transifex. # Pulls translations from Transifex.
pull_translations: pull_translations:
tx pull -t -f --mode reviewed --languages=$(transifex_langs) tx pull -f --mode reviewed --languages=$(transifex_langs)
# This target is used by Travis. # This target is used by Travis.
validate-no-uncommitted-package-lock-changes: validate-no-uncommitted-package-lock-changes:

View File

@@ -14,7 +14,7 @@ A generic header for Open edX micro-frontend applications.
Requirements Requirements
************ ************
This component uses ``@edx/frontend-platform`` services such as i18n, analytics, configuration, and the ``AppContext`` React component, and expects that it has been loaded into a micro-frontend that has been properly initialized via ``@edx/frontend-platform``'s ``initialize`` function. `Please visit the frontend template application to see an example. <https://github.com/openedx/frontend-template-application/blob/master/src/index.jsx>`_ This component uses ``@edx/frontend-platform`` services such as i18n, analytics, configuration, and the ``AppContext`` React component, and expects that it has been loaded into a micro-frontend that has been properly initialized via ``@edx/frontend-platform``'s ``initialize`` function. `Please visit the frontend template application to see an example. <https://github.com/edx/frontend-template-application/blob/master/src/index.jsx>`_
Environment Variables Environment Variables
===================== =====================
@@ -26,8 +26,6 @@ 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.
@@ -55,8 +53,8 @@ This library has the following exports:
Examples Examples
======== ========
* `An example of component and messages usage. <https://github.com/openedx/frontend-template-application/blob/3355bb3a96232390e9056f35b06ffa8f105ed7ca/src/index.jsx#L21>`_ * `An example of component and messages usage. <https://github.com/edx/frontend-template-application/blob/3355bb3a96232390e9056f35b06ffa8f105ed7ca/src/index.jsx#L21>`_
* `An example of SCSS file usage. <https://github.com/openedx/frontend-template-application/blob/3cd5485bf387b8c479baf6b02bf59e3061dc3465/src/index.scss#L8>`_ * `An example of SCSS file usage. <https://github.com/edx/frontend-template-application/blob/3cd5485bf387b8c479baf6b02bf59e3061dc3465/src/index.scss#L8>`_
*********** ***********
@@ -65,7 +63,7 @@ Development
Install dependencies:: Install dependencies::
npm ci npm i
Start the development server:: Start the development server::

39973
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -24,53 +24,53 @@
}, },
"repository": { "repository": {
"type": "git", "type": "git",
"url": "git+https://github.com/openedx/frontend-component-header.git" "url": "git+https://github.com/edx/frontend-component-header.git"
}, },
"author": "edX", "author": "edX",
"license": "AGPL-3.0", "license": "AGPL-3.0",
"bugs": { "bugs": {
"url": "https://github.com/openedx/frontend-component-header/issues" "url": "https://github.com/edx/frontend-component-header/issues"
}, },
"homepage": "https://github.com/openedx/frontend-component-header#readme", "homepage": "https://github.com/edx/frontend-component-header#readme",
"devDependencies": { "devDependencies": {
"@edx/brand": "npm:@edx/brand-openedx@1.2.0", "@edx/brand": "npm:@edx/brand-openedx@1.1.0",
"@edx/browserslist-config": "^1.1.1", "@edx/frontend-build": "9.2.2",
"@edx/frontend-build": "12.9.17", "@edx/frontend-platform": "1.15.1",
"@edx/frontend-platform": "5.4.0", "@edx/paragon": "19.20.0",
"@edx/reactifex": "^2.1.1", "codecov": "3.8.3",
"@testing-library/dom": "9.3.3",
"@testing-library/jest-dom": "5.17.0",
"@testing-library/react": "10.4.9",
"@wojtekmaj/enzyme-adapter-react-17": "0.8.0",
"enzyme": "3.11.0", "enzyme": "3.11.0",
"husky": "8.0.3", "enzyme-adapter-react-16": "1.15.6",
"jest": "29.7.0", "husky": "7.0.4",
"jest-chain": "1.1.6",
"prop-types": "15.8.1", "prop-types": "15.8.1",
"react": "17.0.2", "react": "16.14.0",
"react-dom": "17.0.2", "react-dom": "16.14.0",
"react-redux": "7.2.9", "react-redux": "7.2.8",
"react-router-dom": "6.16.0", "react-router-dom": "5.3.1",
"react-test-renderer": "17.0.2", "react-test-renderer": "16.14.0",
"redux": "4.2.1", "reactifex": "1.1.1",
"redux-saga": "1.2.3" "redux": "4.2.0",
"redux-saga": "1.1.3",
"@testing-library/dom": "7.31.2",
"@testing-library/jest-dom": "5.16.4",
"jest": "27.5.1",
"jest-chain": "1.1.5",
"@testing-library/react": "10.4.9"
}, },
"dependencies": { "dependencies": {
"@edx/paragon": "21.1.10",
"@fortawesome/fontawesome-svg-core": "6.4.2",
"@fortawesome/free-brands-svg-icons": "6.4.2",
"@fortawesome/free-regular-svg-icons": "6.4.2",
"@fortawesome/free-solid-svg-icons": "6.4.2",
"@fortawesome/react-fontawesome": "^0.2.0",
"axios-mock-adapter": "1.21.5",
"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.2",
"@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.1.14"
}, },
"peerDependencies": { "peerDependencies": {
"@edx/frontend-platform": "^4.0.0 || ^5.0.0", "@edx/frontend-platform": "^1.8.0",
"@edx/paragon": ">= 7.0.0 < 20.0.0",
"prop-types": "^15.5.10", "prop-types": "^15.5.10",
"react": "^16.9.0 || ^17.0.0", "react": "^16.9.0",
"react-dom": "^16.9.0 || ^17.0.0" "react-dom": "^16.9.0"
} }
} }

View File

@@ -22,11 +22,6 @@
"pin" "pin"
], ],
"automerge": true "automerge": true
},
{
"matchPackagePatterns": ["@edx", "@openedx"],
"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';
const Avatar = ({ function 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 @@ const 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');
}); });
const Header = ({ intl }) => { function Header({ intl }) {
const { authenticatedUser, config } = useContext(AppContext); const { authenticatedUser, config } = useContext(AppContext);
const mainMenu = [ const mainMenu = [
@@ -55,12 +55,12 @@ const Header = ({ intl }) => {
}, },
{ {
type: 'item', type: 'item',
href: `${config.ACCOUNT_PROFILE_URL}/u/${authenticatedUser.username}`, href: `${config.LMS_BASE_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.ACCOUNT_SETTINGS_URL, href: `${config.LMS_BASE_URL}/account/settings`,
content: intl.formatMessage(messages['header.user.menu.account.settings']), content: intl.formatMessage(messages['header.user.menu.account.settings']),
}, },
{ {
@@ -110,7 +110,7 @@ const Header = ({ intl }) => {
</Responsive> </Responsive>
</> </>
); );
}; }
Header.propTypes = { Header.propTypes = {
intl: intlShape.isRequired, intl: intlShape.isRequired,

View File

@@ -1,4 +1,3 @@
/* 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';
@@ -7,31 +6,28 @@ 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 contextValue = { const component = (
authenticatedUser: null, <ResponsiveContext.Provider value={{ width: 1280 }}>
config: { <IntlProvider locale="en" messages={{}}>
LMS_BASE_URL: process.env.LMS_BASE_URL, <AppContext.Provider
SITE_NAME: process.env.SITE_NAME, value={{
LOGIN_URL: process.env.LOGIN_URL, authenticatedUser: null,
LOGOUT_URL: process.env.LOGOUT_URL, config: {
LOGO_URL: process.env.LOGO_URL, LMS_BASE_URL: process.env.LMS_BASE_URL,
}, SITE_NAME: process.env.SITE_NAME,
}; LOGIN_URL: process.env.LOGIN_URL,
const component = <HeaderComponent width={{ width: 1280 }} contextValue={contextValue} />; LOGOUT_URL: process.env.LOGOUT_URL,
LOGO_URL: process.env.LOGO_URL,
},
}}
>
<Header />
</AppContext.Provider>
</IntlProvider>
</ResponsiveContext.Provider>
);
const wrapper = TestRenderer.create(component); const wrapper = TestRenderer.create(component);
@@ -39,22 +35,31 @@ describe('<Header />', () => {
}); });
it('renders correctly for authenticated desktop', () => { it('renders correctly for authenticated desktop', () => {
const contextValue = { const component = (
authenticatedUser: { <ResponsiveContext.Provider value={{ width: 1280 }}>
userId: 'abc123', <IntlProvider locale="en" messages={{}}>
username: 'edX', <AppContext.Provider
roles: [], value={{
administrator: false, authenticatedUser: {
}, userId: 'abc123',
config: { username: 'edX',
LMS_BASE_URL: process.env.LMS_BASE_URL, roles: [],
SITE_NAME: process.env.SITE_NAME, administrator: false,
LOGIN_URL: process.env.LOGIN_URL, },
LOGOUT_URL: process.env.LOGOUT_URL, config: {
LOGO_URL: process.env.LOGO_URL, LMS_BASE_URL: process.env.LMS_BASE_URL,
}, SITE_NAME: process.env.SITE_NAME,
}; LOGIN_URL: process.env.LOGIN_URL,
const component = <HeaderComponent width={{ width: 1280 }} contextValue={contextValue} />; LOGOUT_URL: process.env.LOGOUT_URL,
LOGO_URL: process.env.LOGO_URL,
},
}}
>
<Header />
</AppContext.Provider>
</IntlProvider>
</ResponsiveContext.Provider>
);
const wrapper = TestRenderer.create(component); const wrapper = TestRenderer.create(component);
@@ -62,17 +67,26 @@ describe('<Header />', () => {
}); });
it('renders correctly for anonymous mobile', () => { it('renders correctly for anonymous mobile', () => {
const contextValue = { const component = (
authenticatedUser: null, <ResponsiveContext.Provider value={{ width: 500 }}>
config: { <IntlProvider locale="en" messages={{}}>
LMS_BASE_URL: process.env.LMS_BASE_URL, <AppContext.Provider
SITE_NAME: process.env.SITE_NAME, value={{
LOGIN_URL: process.env.LOGIN_URL, authenticatedUser: null,
LOGOUT_URL: process.env.LOGOUT_URL, config: {
LOGO_URL: process.env.LOGO_URL, LMS_BASE_URL: process.env.LMS_BASE_URL,
}, SITE_NAME: process.env.SITE_NAME,
}; LOGIN_URL: process.env.LOGIN_URL,
const component = <HeaderComponent width={{ width: 500 }} contextValue={contextValue} />; LOGOUT_URL: process.env.LOGOUT_URL,
LOGO_URL: process.env.LOGO_URL,
},
}}
>
<Header />
</AppContext.Provider>
</IntlProvider>
</ResponsiveContext.Provider>
);
const wrapper = TestRenderer.create(component); const wrapper = TestRenderer.create(component);
@@ -80,22 +94,31 @@ describe('<Header />', () => {
}); });
it('renders correctly for authenticated mobile', () => { it('renders correctly for authenticated mobile', () => {
const contextValue = { const component = (
authenticatedUser: { <ResponsiveContext.Provider value={{ width: 500 }}>
userId: 'abc123', <IntlProvider locale="en" messages={{}}>
username: 'edX', <AppContext.Provider
roles: [], value={{
administrator: false, authenticatedUser: {
}, userId: 'abc123',
config: { username: 'edX',
LMS_BASE_URL: process.env.LMS_BASE_URL, roles: [],
SITE_NAME: process.env.SITE_NAME, administrator: false,
LOGIN_URL: process.env.LOGIN_URL, },
LOGOUT_URL: process.env.LOGOUT_URL, config: {
LOGO_URL: process.env.LOGO_URL, LMS_BASE_URL: process.env.LMS_BASE_URL,
}, SITE_NAME: process.env.SITE_NAME,
}; LOGIN_URL: process.env.LOGIN_URL,
const component = <HeaderComponent width={{ width: 500 }} contextValue={contextValue} />; LOGOUT_URL: process.env.LOGOUT_URL,
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,25 +1,29 @@
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
const Logo = ({ src, alt, ...attributes }) => ( function Logo({ src, alt, ...attributes }) {
<img src={src} alt={alt} {...attributes} /> return (
); <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,
}; };
const LinkedLogo = ({ function LinkedLogo({
href, href,
src, src,
alt, alt,
...attributes ...attributes
}) => ( }) {
<a href={href} {...attributes}> return (
<img className="d-block" src={src} alt={alt} /> <a href={href} {...attributes}>
</a> <img className="d-block" src={src} alt={alt} />
); </a>
);
}
LinkedLogo.propTypes = { LinkedLogo.propTypes = {
href: PropTypes.string.isRequired, href: PropTypes.string.isRequired,

View File

@@ -2,10 +2,12 @@ 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';
const MenuTrigger = ({ tag, className, ...attributes }) => React.createElement(tag, { function MenuTrigger({ tag, className, ...attributes }) {
className: `menu-trigger ${className}`, return React.createElement(tag, {
...attributes, className: `menu-trigger ${className}`,
}); ...attributes,
});
}
MenuTrigger.propTypes = { MenuTrigger.propTypes = {
tag: PropTypes.string, tag: PropTypes.string,
className: PropTypes.string, className: PropTypes.string,
@@ -16,10 +18,12 @@ MenuTrigger.defaultProps = {
}; };
const MenuTriggerType = <MenuTrigger />.type; const MenuTriggerType = <MenuTrigger />.type;
const MenuContent = ({ tag, className, ...attributes }) => React.createElement(tag, { function MenuContent({ tag, className, ...attributes }) {
className: ['menu-content', className].join(' '), return React.createElement(tag, {
...attributes, className: ['menu-content', className].join(' '),
}); ...attributes,
});
}
MenuContent.propTypes = { MenuContent.propTypes = {
tag: PropTypes.string, tag: PropTypes.string,
className: PropTypes.string, className: PropTypes.string,

99
src/StudioHeader.jsx Normal file
View File

@@ -0,0 +1,99 @@
import React, { useContext } from 'react';
import PropTypes from 'prop-types';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { AppContext } from '@edx/frontend-platform/react';
import {
APP_CONFIG_INITIALIZED,
ensureConfig,
mergeConfig,
subscribe,
} from '@edx/frontend-platform';
import DesktopHeader from './DesktopHeader';
import messages from './Header.messages';
ensureConfig([
'STUDIO_BASE_URL',
'LOGOUT_URL',
'LOGIN_URL',
'SITE_NAME',
'LOGO_URL',
'ORDER_HISTORY_URL',
], 'StudioHeader component');
subscribe(APP_CONFIG_INITIALIZED, () => {
mergeConfig({
AUTHN_MINIMAL_HEADER: !!process.env.AUTHN_MINIMAL_HEADER,
}, 'StudioHeader additional config');
});
function StudioHeader({ intl, mainMenu, appMenu }) {
const { authenticatedUser, config } = useContext(AppContext);
const userMenu = authenticatedUser === null ? [] : [
{
type: 'item',
href: `${config.STUDIO_BASE_URL}`,
content: intl.formatMessage(messages['header.user.menu.studio.home']),
},
{
type: 'item',
href: `${config.STUDIO_BASE_URL}/maintenance`,
content: intl.formatMessage(messages['header.user.menu.studio.maintenance']),
},
{
type: 'item',
href: config.LOGOUT_URL,
content: intl.formatMessage(messages['header.user.menu.logout']),
},
];
const props = {
logo: config.LOGO_URL,
logoAltText: config.SITE_NAME,
logoDestination: config.STUDIO_BASE_URL,
loggedIn: authenticatedUser !== null,
username: authenticatedUser !== null ? authenticatedUser.username : null,
avatar: authenticatedUser !== null ? authenticatedUser.avatar : null,
mainMenu,
userMenu,
appMenu,
loggedOutItems: [],
};
return <DesktopHeader {...props} />;
}
StudioHeader.propTypes = {
intl: intlShape.isRequired,
appMenu: PropTypes.shape(
{
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 = {
appMenu: null,
mainMenu: [],
};
export default injectIntl(StudioHeader);

135
src/StudioHeader.test.jsx Normal file
View File

@@ -0,0 +1,135 @@
import React from 'react';
import { IntlProvider } from '@edx/frontend-platform/i18n';
import TestRenderer from 'react-test-renderer';
import { AppContext } from '@edx/frontend-platform/react';
import { StudioHeader } from './index';
describe('<StudioHeader />', () => {
it('renders correctly', () => {
const component = (
<IntlProvider locale="en" messages={{}}>
<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 />
</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={{}}>
<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);
expect(wrapper.toJSON()).toMatchSnapshot();
});
it('renders correctly with the optional main menu', () => {
const mainMenu = [
{
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={{}}>
<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 mainMenu={mainMenu} />
</AppContext.Provider>
</IntlProvider>
);
const wrapper = TestRenderer.create(component);
expect(wrapper.toJSON()).toMatchSnapshot();
});
});

View File

@@ -0,0 +1,425 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`<StudioHeader /> renders correctly 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"
/>
<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
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"
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>
</header>
`;
exports[`<StudioHeader /> renders correctly with the optional app 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"
/>
<nav
aria-label="App"
className="nav app-nav"
>
<div
className="menu null"
onKeyDown={[Function]}
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
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
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"
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>
</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
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"
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>
</header>
`;

View File

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

View File

@@ -1,33 +1,33 @@
{ {
"general.register.sentenceCase": "التسجيل", "general.register.sentenceCase": "التسجيل",
"general.signIn.sentenceCase": "تسجيل الدخول", "general.signIn.sentenceCase": "تسجيل الدخول",
"header.links.courses": "المساقات", "header.links.courses": "مساقات",
"header.links.programs": "البرامج", "header.links.programs": "برامج",
"header.links.content.search": "اكتشف الجديد", "header.links.content.search": "استكشف الجديد",
"header.links.schools": "المدارس و الشركاء", "header.links.schools": "المدارس والشركاء",
"header.user.menu.dashboard": "لوحة المعلومات", "header.user.menu.dashboard": "لوحة المعلومات",
"header.user.menu.profile": "الملف الشخصي", "header.user.menu.profile": "الملف الشخصي",
"header.user.menu.account.settings": "الحساب", "header.user.menu.account.settings": "حساب",
"header.user.menu.order.history": "سجل الطلبيات", "header.user.menu.order.history": "سجل الطلبات",
"header.user.menu.logout": "تسجيل الخروج", "header.user.menu.logout": "تسجيل الخروج",
"header.user.menu.login": "تسجيل الدخول", "header.user.menu.login": "تسجيل الدخول",
"header.user.menu.register": "التسجيل", "header.user.menu.register": "تسجيل ",
"header.user.menu.studio.home": "صفحة الاستوديو الرئيسية", "header.user.menu.studio.home": "Studio Home",
"header.user.menu.studio.maintenance": "الصيانة", "header.user.menu.studio.maintenance": "Maintenance",
"header.label.account.nav": "الحساب", "header.label.account.nav": "حساب",
"header.label.account.menu": "قائمة الحساب", "header.label.account.menu": "قائمة الحساب",
"header.label.account.menu.for": "قائمة حساب المستخدم {username}", "header.label.account.menu.for": "قائمة الحساب للمستخدم {username}",
"header.label.main.nav": "القا|مة الرئيسية", "header.label.main.nav": "الرئيسية",
"header.label.main.menu": "القائمة الرئيسية", "header.label.main.menu": "القائمة الرئيسية",
"header.label.main.header": "الرئيسية", "header.label.main.header": "الرئيسية",
"header.label.secondary.nav": "القائمة الثانوية", "header.label.secondary.nav": "فرعي",
"header.label.skip.nav": "التخطي إلى المحتوى الرئيسي", "header.label.skip.nav": "التخطي إلى المحتوى الرئيسي",
"header.label.app.nav": "تطبيق", "header.label.app.nav": "App",
"header.menu.dashboard.label": "لوحة المعلومات", "header.menu.dashboard.label": "لوحة المعلومات",
"header.help.label": "المساعدة", "header.help.label": "مساعدة",
"header.menu.profile.label": "الملف الشخصي", "header.menu.profile.label": "الملف الشخصي",
"header.menu.account.label": "الحساب", "header.menu.account.label": "حساب",
"header.menu.orderHistory.label": "سجل الطلبيات", "header.menu.orderHistory.label": "سجل الطلبات",
"header.navigation.skipNavLink": "التخطي إلى المحتوى الرئيسي", "header.navigation.skipNavLink": "التخطي إلى المحتوى الرئيسي",
"header.menu.signOut.label": "تسجيل الخروج" "header.menu.signOut.label": "تسجيل الخروج"
} }

View File

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

View File

@@ -1,33 +0,0 @@
{
"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

@@ -12,8 +12,8 @@
"header.user.menu.logout": "Cerrar sesión", "header.user.menu.logout": "Cerrar sesión",
"header.user.menu.login": "Login", "header.user.menu.login": "Login",
"header.user.menu.register": "Registrarse", "header.user.menu.register": "Registrarse",
"header.user.menu.studio.home": "Inicio Studio", "header.user.menu.studio.home": "Studio Home",
"header.user.menu.studio.maintenance": "Mantenimiento", "header.user.menu.studio.maintenance": "Maintenance",
"header.label.account.nav": "Cuenta", "header.label.account.nav": "Cuenta",
"header.label.account.menu": "Menú de la cuenta", "header.label.account.menu": "Menú de la cuenta",
"header.label.account.menu.for": "Menú de la cuenta para {username}", "header.label.account.menu.for": "Menú de la cuenta para {username}",
@@ -22,7 +22,7 @@
"header.label.main.header": "Principal", "header.label.main.header": "Principal",
"header.label.secondary.nav": "Secondary", "header.label.secondary.nav": "Secondary",
"header.label.skip.nav": "Ir al contenido principal", "header.label.skip.nav": "Ir al contenido principal",
"header.label.app.nav": "Aplicación", "header.label.app.nav": "App",
"header.menu.dashboard.label": "Panel de Control", "header.menu.dashboard.label": "Panel de Control",
"header.help.label": "Ayuda", "header.help.label": "Ayuda",
"header.menu.profile.label": "Perfil", "header.menu.profile.label": "Perfil",

View File

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

View File

@@ -1,33 +0,0 @@
{
"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

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

View File

@@ -1,33 +0,0 @@
{
"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

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

View File

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

View File

@@ -1,33 +0,0 @@
{
"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

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

View File

@@ -1,33 +1 @@
{ {}
"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

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

View File

@@ -1,33 +1 @@
{ {}
"general.register.sentenceCase": "Register",
"general.signIn.sentenceCase": "Увійти",
"header.links.courses": "Курси",
"header.links.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": "Меню облікового запису",
"header.label.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": "Перейти до головного змісту",
"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": "Перейти до головного змісту.",
"header.menu.signOut.label": "Sign Out"
}

View File

@@ -1,7 +1,7 @@
import Header from './Header'; import Header from './Header';
import LearningHeader from './learning-header/LearningHeader'; import LearningHeader from './learning-header/LearningHeader';
import messages from './i18n/index'; import messages from './i18n/index';
import StudioHeader from './studio-header'; import StudioHeader from './StudioHeader';
export { LearningHeader, messages, StudioHeader }; export { LearningHeader, messages, StudioHeader };

View File

@@ -3,7 +3,6 @@ $blue: #007db8;
$white: #fff; $white: #fff;
@import './Menu/menu.scss'; @import './Menu/menu.scss';
@import './studio-header/header.scss';
.dropdown-item a { .dropdown-item a {
text-decoration: none; text-decoration: none;

View File

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

View File

@@ -3,13 +3,14 @@ import PropTypes from 'prop-types';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faUserCircle } from '@fortawesome/free-solid-svg-icons'; import { faUserCircle } from '@fortawesome/free-solid-svg-icons';
import { getConfig } from '@edx/frontend-platform'; import { getConfig } from '@edx/frontend-platform';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n'; import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { Dropdown } from '@edx/paragon'; import { Dropdown } from '@edx/paragon';
import messages from './messages'; import messages from './messages';
const AuthenticatedUserDropdown = ({ intl, username }) => { function 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)}
@@ -18,8 +19,8 @@ const AuthenticatedUserDropdown = ({ intl, username }) => {
return ( return (
<> <>
<a className="text-gray-700" href={`${getConfig().SUPPORT_URL}`}>{intl.formatMessage(messages.help)}</a> <a className="text-gray-700 mr-3" href={`${getConfig().SUPPORT_URL}`}>{intl.formatMessage(messages.help)}</a>
<Dropdown className="user-dropdown ml-3"> <Dropdown className="user-dropdown">
<Dropdown.Toggle variant="outline-primary"> <Dropdown.Toggle variant="outline-primary">
<FontAwesomeIcon icon={faUserCircle} className="d-md-none" size="lg" /> <FontAwesomeIcon icon={faUserCircle} className="d-md-none" size="lg" />
<span data-hj-suppress className="d-none d-md-inline"> <span data-hj-suppress className="d-none d-md-inline">
@@ -28,10 +29,10 @@ const 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().ACCOUNT_PROFILE_URL}/u/${username}`}> <Dropdown.Item href={`${getConfig().LMS_BASE_URL}/u/${username}`}>
{intl.formatMessage(messages.profile)} {intl.formatMessage(messages.profile)}
</Dropdown.Item> </Dropdown.Item>
<Dropdown.Item href={getConfig().ACCOUNT_SETTINGS_URL}> <Dropdown.Item href={`${getConfig().LMS_BASE_URL}/account/settings`}>
{intl.formatMessage(messages.account)} {intl.formatMessage(messages.account)}
</Dropdown.Item> </Dropdown.Item>
{ getConfig().ORDER_HISTORY_URL && ( { getConfig().ORDER_HISTORY_URL && (
@@ -46,7 +47,7 @@ const AuthenticatedUserDropdown = ({ intl, username }) => {
</Dropdown> </Dropdown>
</> </>
); );
}; }
AuthenticatedUserDropdown.propTypes = { AuthenticatedUserDropdown.propTypes = {
intl: intlShape.isRequired, intl: intlShape.isRequired,

View File

@@ -8,16 +8,18 @@ import AnonymousUserMenu from './AnonymousUserMenu';
import AuthenticatedUserDropdown from './AuthenticatedUserDropdown'; import AuthenticatedUserDropdown from './AuthenticatedUserDropdown';
import messages from './messages'; import messages from './messages';
const LinkedLogo = ({ function LinkedLogo({
href, href,
src, src,
alt, alt,
...attributes ...attributes
}) => ( }) {
<a href={href} {...attributes}> return (
<img className="d-block" src={src} alt={alt} /> <a href={href} {...attributes}>
</a> <img className="d-block" src={src} alt={alt} />
); </a>
);
}
LinkedLogo.propTypes = { LinkedLogo.propTypes = {
href: PropTypes.string.isRequired, href: PropTypes.string.isRequired,
@@ -25,9 +27,9 @@ LinkedLogo.propTypes = {
alt: PropTypes.string.isRequired, alt: PropTypes.string.isRequired,
}; };
const LearningHeader = ({ function LearningHeader({
courseOrg, courseNumber, courseTitle, intl, showUserDropdown, courseOrg, courseNumber, courseTitle, intl, showUserDropdown,
}) => { }) {
const { authenticatedUser } = useContext(AppContext); const { authenticatedUser } = useContext(AppContext);
const headerLogo = ( const headerLogo = (
@@ -49,17 +51,17 @@ const LearningHeader = ({
<span className="d-block m-0 font-weight-bold course-title">{courseTitle}</span> <span className="d-block m-0 font-weight-bold course-title">{courseTitle}</span>
</div> </div>
{showUserDropdown && authenticatedUser && ( {showUserDropdown && authenticatedUser && (
<AuthenticatedUserDropdown <AuthenticatedUserDropdown
username={authenticatedUser.username} username={authenticatedUser.username}
/> />
)} )}
{showUserDropdown && !authenticatedUser && ( {showUserDropdown && !authenticatedUser && (
<AnonymousUserMenu /> <AnonymousUserMenu />
)} )}
</div> </div>
</header> </header>
); );
}; }
LearningHeader.propTypes = { LearningHeader.propTypes = {
courseOrg: PropTypes.string, courseOrg: PropTypes.string,

View File

@@ -12,7 +12,7 @@ describe('Header', () => {
it('displays user button', () => { it('displays user button', () => {
render(<Header />); render(<Header />);
expect(screen.getByText(authenticatedUser.username)).toBeInTheDocument(); expect(screen.getByRole('button')).toHaveTextContent(authenticatedUser.username);
}); });
it('displays course data', () => { it('displays course data', () => {

View File

@@ -3,7 +3,7 @@
import Enzyme from 'enzyme'; import Enzyme from 'enzyme';
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import Adapter from '@wojtekmaj/enzyme-adapter-react-17'; import Adapter from 'enzyme-adapter-react-16';
import '@testing-library/jest-dom'; import '@testing-library/jest-dom';
import '@testing-library/jest-dom/extend-expect'; import '@testing-library/jest-dom/extend-expect';
import 'babel-polyfill'; import 'babel-polyfill';
@@ -22,8 +22,6 @@ 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';
@@ -104,14 +102,16 @@ function render(
...renderOptions ...renderOptions
} = {}, } = {},
) { ) {
const Wrapper = ({ children }) => ( function 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,

View File

@@ -1,24 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
const BrandNav = ({
studioBaseUrl,
logo,
logoAltText,
}) => (
<a href={studioBaseUrl}>
<img
src={logo}
alt={logoAltText}
className="d-block logo"
/>
</a>
);
BrandNav.propTypes = {
studioBaseUrl: PropTypes.string.isRequired,
logo: PropTypes.string.isRequired,
logoAltText: PropTypes.string.isRequired,
};
export default BrandNav;

View File

@@ -1,54 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import {
OverlayTrigger,
Tooltip,
} from '@edx/paragon';
import messages from './messages';
const CourseLockUp = ({
outlineLink,
org,
number,
title,
// injected
intl,
}) => (
<OverlayTrigger
placement="bottom"
overlay={(
<Tooltip id="course-lock-up">
{title}
</Tooltip>
)}
>
<a
className="course-title-lockup w-25 mr-2"
href={outlineLink}
aria-label={intl.formatMessage(messages['header.label.courseOutline'])}
data-testid="course-lock-up-block"
>
<span className="d-block small m-0 text-gray-800" data-testid="course-org-number">{org} {number}</span>
<span className="d-block m-0 font-weight-bold text-gray-800" data-testid="course-title">{title}</span>
</a>
</OverlayTrigger>
);
CourseLockUp.propTypes = {
number: PropTypes.string,
org: PropTypes.string,
title: PropTypes.string,
outlineLink: PropTypes.string,
// injected
intl: intlShape.isRequired,
};
CourseLockUp.defaultProps = {
number: null,
org: null,
title: null,
outlineLink: null,
};
export default injectIntl(CourseLockUp);

View File

@@ -1,155 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
import {
ActionRow,
Button,
Container,
Nav,
Row,
} from '@edx/paragon';
import { Close, MenuIcon } from '@edx/paragon/icons';
import CourseLockUp from './CourseLockUp';
import UserMenu from './UserMenu';
import BrandNav from './BrandNav';
import NavDropdownMenu from './NavDropdownMenu';
const HeaderBody = ({
logo,
logoAltText,
number,
org,
title,
username,
isAdmin,
studioBaseUrl,
logoutUrl,
authenticatedUserAvatar,
isMobile,
setModalPopupTarget,
toggleModalPopup,
isModalPopupOpen,
isHiddenMainMenu,
mainMenuDropdowns,
outlineLink,
}) => {
const renderBrandNav = (
<BrandNav
{...{
studioBaseUrl,
logo,
logoAltText,
}}
/>
);
return (
<Container size="xl" className="px-4">
<ActionRow as="header">
{isHiddenMainMenu ? (
<Row className="flex-nowrap ml-4">
{renderBrandNav}
</Row>
) : (
<>
{isMobile ? (
<Button
ref={setModalPopupTarget}
className="d-inline-flex align-items-center"
variant="tertiary"
onClick={toggleModalPopup}
iconBefore={isModalPopupOpen ? Close : MenuIcon}
data-testid="mobile-menu-button"
>
Menu
</Button>
) : (
<Row className="flex-nowrap m-0">
{renderBrandNav}
<CourseLockUp
{...{
outlineLink,
number,
org,
title,
}}
/>
</Row>
)}
{isMobile ? (
<>
<ActionRow.Spacer />
{renderBrandNav}
</>
) : (
<Nav data-testid="desktop-menu" className="ml-4">
{mainMenuDropdowns.map(dropdown => {
const { id, buttonTitle, items } = dropdown;
return (
<NavDropdownMenu {...{ id, buttonTitle, items }} />
);
})}
</Nav>
)}
</>
)}
<ActionRow.Spacer />
<Nav>
<UserMenu
{...{
username,
studioBaseUrl,
logoutUrl,
authenticatedUserAvatar,
isAdmin,
}}
/>
</Nav>
</ActionRow>
</Container>
);
};
HeaderBody.propTypes = {
studioBaseUrl: PropTypes.string.isRequired,
logoutUrl: PropTypes.string.isRequired,
setModalPopupTarget: PropTypes.func.isRequired,
toggleModalPopup: PropTypes.func.isRequired,
isModalPopupOpen: PropTypes.bool.isRequired,
number: PropTypes.string,
org: PropTypes.string,
title: PropTypes.string,
logo: PropTypes.string,
logoAltText: PropTypes.string,
authenticatedUserAvatar: PropTypes.string,
username: PropTypes.string,
isAdmin: PropTypes.bool,
isMobile: PropTypes.bool,
isHiddenMainMenu: PropTypes.bool,
mainMenuDropdowns: PropTypes.arrayOf(PropTypes.shape({
id: PropTypes.string,
buttonTitle: PropTypes.string,
items: PropTypes.arrayOf(PropTypes.shape({
href: PropTypes.string,
title: PropTypes.string,
})),
})),
outlineLink: PropTypes.string,
};
HeaderBody.defaultProps = {
logo: null,
logoAltText: null,
number: '',
org: '',
title: '',
authenticatedUserAvatar: null,
username: null,
isAdmin: false,
isMobile: false,
isHiddenMainMenu: false,
mainMenuDropdowns: [],
outlineLink: null,
};
export default HeaderBody;

View File

@@ -1,76 +0,0 @@
import React, { useState } from 'react';
import PropTypes from 'prop-types';
import { useToggle, ModalPopup } from '@edx/paragon';
import HeaderBody from './HeaderBody';
import MobileMenu from './MobileMenu';
const MobileHeader = ({
mainMenuDropdowns,
...props
}) => {
const [isOpen, , close, toggle] = useToggle(false);
const [target, setTarget] = useState(null);
return (
<>
<HeaderBody
{...props}
isMobile
setModalPopupTarget={setTarget}
toggleModalPopup={toggle}
isModalPopupOpen={isOpen}
/>
<ModalPopup
hasArrow
placement="bottom"
positionRef={target}
isOpen={isOpen}
onClose={close}
onEscapeKey={close}
className="mobile-menu-container"
>
<MobileMenu {...{ mainMenuDropdowns }} />
</ModalPopup>
</>
);
};
MobileHeader.propTypes = {
studioBaseUrl: PropTypes.string.isRequired,
logoutUrl: PropTypes.string.isRequired,
setModalPopupTarget: PropTypes.func.isRequired,
toggleModalPopup: PropTypes.func.isRequired,
isModalPopupOpen: PropTypes.bool.isRequired,
number: PropTypes.string,
org: PropTypes.string,
title: PropTypes.string,
logo: PropTypes.string,
logoAltText: PropTypes.string,
authenticatedUserAvatar: PropTypes.string,
username: PropTypes.string,
isAdmin: PropTypes.bool,
mainMenuDropdowns: PropTypes.arrayOf(PropTypes.shape({
id: PropTypes.string,
buttonTitle: PropTypes.string,
items: PropTypes.arrayOf(PropTypes.shape({
href: PropTypes.string,
title: PropTypes.string,
})),
})),
outlineLink: PropTypes.string,
};
MobileHeader.defaultProps = {
logo: null,
logoAltText: null,
number: null,
org: null,
title: null,
authenticatedUserAvatar: null,
username: null,
isAdmin: false,
mainMenuDropdowns: [],
outlineLink: null,
};
export default MobileHeader;

View File

@@ -1,51 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Collapsible } from '@edx/paragon';
const MobileMenu = ({
mainMenuDropdowns,
}) => (
<div
className="ml-4 p-2 bg-light-100 border border-gray-200 small rounded"
data-testid="mobile-menu"
>
<div>
{mainMenuDropdowns.map(dropdown => {
const { id, buttonTitle, items } = dropdown;
return (
<Collapsible
className="border-light-100"
title={buttonTitle}
key={id}
>
<ul className="p-0" style={{ listStyleType: 'none' }}>
{items.map(item => (
<li className="mobile-menu-item">
<a href={item.href}>
{item.title}
</a>
</li>
))}
</ul>
</Collapsible>
);
})}
</div>
</div>
);
MobileMenu.propTypes = {
mainMenuDropdowns: PropTypes.arrayOf(PropTypes.shape({
id: PropTypes.string,
buttonTitle: PropTypes.string,
items: PropTypes.arrayOf(PropTypes.shape({
href: PropTypes.string,
title: PropTypes.string,
})),
})),
};
MobileMenu.defaultProps = {
mainMenuDropdowns: [],
};
export default MobileMenu;

View File

@@ -1,38 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
import {
Dropdown,
DropdownButton,
} from '@edx/paragon';
const NavDropdownMenu = ({
id,
buttonTitle,
items,
}) => (
<DropdownButton
id={id}
title={buttonTitle}
variant="tertiary"
>
{items.map(item => (
<Dropdown.Item
href={item.href}
className="small"
>
{item.title}
</Dropdown.Item>
))}
</DropdownButton>
);
NavDropdownMenu.propTypes = {
id: PropTypes.string.isRequired,
buttonTitle: PropTypes.string.isRequired,
items: PropTypes.arrayOf(PropTypes.shape({
href: PropTypes.string,
title: PropTypes.string,
})).isRequired,
};
export default NavDropdownMenu;

View File

@@ -1,74 +0,0 @@
import React, { useContext } from 'react';
import PropTypes from 'prop-types';
import Responsive from 'react-responsive';
import { AppContext } from '@edx/frontend-platform/react';
import { ensureConfig } from '@edx/frontend-platform';
import MobileHeader from './MobileHeader';
import HeaderBody from './HeaderBody';
ensureConfig([
'STUDIO_BASE_URL',
'SITE_NAME',
'LOGOUT_URL',
'LOGIN_URL',
'LOGO_URL',
], 'Studio Header component');
const StudioHeader = ({
number, org, title, isHiddenMainMenu, mainMenuDropdowns, outlineLink,
}) => {
const { authenticatedUser, config } = useContext(AppContext);
const props = {
logo: config.LOGO_URL,
logoAltText: `Studio ${config.SITE_NAME}`,
number,
org,
title,
username: authenticatedUser?.username,
isAdmin: authenticatedUser?.administrator,
authenticatedUserAvatar: authenticatedUser?.avatar,
studioBaseUrl: config.STUDIO_BASE_URL,
logoutUrl: config.LOGOUT_URL,
isHiddenMainMenu,
mainMenuDropdowns,
outlineLink,
};
return (
<>
<Responsive maxWidth={768}>
<MobileHeader {...props} />
</Responsive>
<Responsive minWidth={769}>
<HeaderBody {...props} />
</Responsive>
</>
);
};
StudioHeader.propTypes = {
number: PropTypes.string,
org: PropTypes.string,
title: PropTypes.string.isRequired,
isHiddenMainMenu: PropTypes.bool,
mainMenuDropdowns: PropTypes.arrayOf(PropTypes.shape({
id: PropTypes.string,
buttonTitle: PropTypes.string,
items: PropTypes.arrayOf(PropTypes.shape({
href: PropTypes.string,
title: PropTypes.string,
})),
})),
outlineLink: PropTypes.string,
};
StudioHeader.defaultProps = {
number: '',
org: '',
isHiddenMainMenu: false,
mainMenuDropdowns: [],
outlineLink: null,
};
export default StudioHeader;

View File

@@ -1,197 +0,0 @@
/* eslint-disable react/prop-types */
import React, { useMemo } from 'react';
import {
render,
fireEvent,
waitFor,
} from '@testing-library/react';
import { AppContext } from '@edx/frontend-platform/react';
import { IntlProvider } from '@edx/frontend-platform/i18n';
import { Context as ResponsiveContext } from 'react-responsive';
import StudioHeader from './StudioHeader';
import messages from './messages';
const authenticatedUser = {
userId: 3,
username: 'abc123',
administrator: true,
roles: [],
avatar: '/imges/test.png',
};
let currentUser;
let screenWidth = 1280;
const RootWrapper = ({
...props
}) => {
const appContextValue = useMemo(() => ({
authenticatedUser: currentUser,
config: {
LOGOUT_URL: process.env.LOGOUT_URL,
LOGO_URL: process.env.LOGO_URL,
SITE_NAME: process.env.SITE_NAME,
STUDIO_BASE_URL: process.env.STUDIO_BASE_URL,
LOGIN_URL: process.env.LOGIN_URL,
},
}), []);
const responsiveContextValue = useMemo(() => ({ width: screenWidth }), []);
return (
// eslint-disable-next-line react/jsx-no-constructed-context-values, react/prop-types
<IntlProvider locale="en">
<AppContext.Provider value={appContextValue}>
<ResponsiveContext.Provider value={responsiveContextValue}>
<StudioHeader
{...props}
/>
</ResponsiveContext.Provider>
</AppContext.Provider>
</IntlProvider>
);
};
const props = {
number: '123',
org: 'Ed',
title: 'test',
mainMenuDropdowns: [
{
id: 'testId',
buttonTitle: 'test',
items: [
{
title: 'link',
href: '#',
},
],
},
],
outlineLink: 'tEsTLInK',
};
describe('Header', () => {
beforeEach(() => {
jest.clearAllMocks();
currentUser = authenticatedUser;
});
describe('desktop', () => {
it('course lock up should be visible', () => {
const { getByTestId } = render(<RootWrapper {...props} />);
const courseLockUpBlock = getByTestId('course-lock-up-block');
expect(courseLockUpBlock).toBeVisible();
});
it('mobile menu should not be visible', () => {
const { queryByTestId } = render(<RootWrapper {...props} />);
const mobileMenuButton = queryByTestId('mobile-menu-button');
expect(mobileMenuButton).toBeNull();
});
it('desktop menu should be visible', () => {
const { getByTestId } = render(<RootWrapper {...props} />);
const desktopMenu = getByTestId('desktop-menu');
expect(desktopMenu).toBeVisible();
});
it('should render one dropdown', async () => {
const { getAllByRole, getByText } = render(<RootWrapper {...props} />);
const dropdownMenu = getAllByRole('button')[0];
expect(dropdownMenu).toBeVisible();
await waitFor(() => fireEvent.click(dropdownMenu));
const dropdownOption = getByText('link');
expect(dropdownOption).toBeVisible();
});
it('maintenance should not be in user menu', async () => {
currentUser = { ...authenticatedUser, administrator: false };
const { getAllByRole, queryByText } = render(<RootWrapper {...props} />);
const userMenu = getAllByRole('button')[1];
await waitFor(() => fireEvent.click(userMenu));
const maintenanceButton = queryByText(messages['header.user.menu.maintenance'].defaultMessage);
expect(maintenanceButton).toBeNull();
});
it('user menu should use avatar icon', async () => {
currentUser = { ...authenticatedUser, avatar: null };
const { getByTestId } = render(<RootWrapper {...props} />);
const avatarIcon = getByTestId('avatar-icon');
expect(avatarIcon).toBeVisible();
});
it('should hide nav items if prop isHiddenMainMenu true', async () => {
const initialProps = { ...props, isHiddenMainMenu: true };
const { queryByTestId } = render(<RootWrapper {...initialProps} />);
const desktopMenu = queryByTestId('desktop-menu');
const mobileMenuButton = queryByTestId('mobile-menu-button');
expect(mobileMenuButton).toBeNull();
expect(desktopMenu).toBeNull();
});
});
describe('mobile', () => {
beforeEach(() => { screenWidth = 500; });
it('course lock up should not be visible', async () => {
const { queryByTestId } = render(<RootWrapper {...props} />);
const courseLockUpBlock = queryByTestId('course-lock-up-block');
expect(courseLockUpBlock).toBeNull();
});
it('mobile menu should be visible', async () => {
const { getByTestId } = render(<RootWrapper {...props} />);
const mobileMenuButton = getByTestId('mobile-menu-button');
expect(mobileMenuButton).toBeVisible();
await waitFor(() => fireEvent.click(mobileMenuButton));
const mobileMenu = getByTestId('mobile-menu');
expect(mobileMenu).toBeVisible();
});
it('desktop menu should not be visible', () => {
const { queryByTestId } = render(<RootWrapper {...props} />);
const desktopMenu = queryByTestId('desktop-menu');
expect(desktopMenu).toBeNull();
});
it('maintenance should be in user menu', async () => {
const { getAllByRole, getByText } = render(<RootWrapper {...props} />);
const userMenu = getAllByRole('button')[1];
await waitFor(() => fireEvent.click(userMenu));
const maintenanceButton = getByText(messages['header.user.menu.maintenance'].defaultMessage);
expect(maintenanceButton).toBeVisible();
});
it('user menu should use avatar image', async () => {
const { getByTestId } = render(<RootWrapper {...props} />);
const avatarImage = getByTestId('avatar-image');
expect(avatarImage).toBeVisible();
});
it('should hide nav items if prop isHiddenMainMenu true', async () => {
const initialProps = { ...props, isHiddenMainMenu: true };
const { queryByTestId } = render(<RootWrapper {...initialProps} />);
const desktopMenu = queryByTestId('desktop-menu');
const mobileMenuButton = queryByTestId('mobile-menu-button');
expect(mobileMenuButton).toBeNull();
expect(desktopMenu).toBeNull();
});
});
});

View File

@@ -1,69 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import {
Avatar,
} from '@edx/paragon';
import NavDropdownMenu from './NavDropdownMenu';
import getUserMenuItems from './utils';
const UserMenu = ({
username,
studioBaseUrl,
logoutUrl,
authenticatedUserAvatar,
isMobile,
isAdmin,
// injected
intl,
}) => {
const avatar = authenticatedUserAvatar ? (
<img
className="d-block w-100 h-100"
src={authenticatedUserAvatar}
alt={username}
data-testid="avatar-image"
/>
) : (
<Avatar
size="sm"
className="mr-2"
alt={username}
data-testid="avatar-icon"
/>
);
const title = isMobile ? avatar : <>{avatar}{username}</>;
return (
<NavDropdownMenu
buttonTitle={title}
id="user-dropdown-menu"
items={getUserMenuItems({
studioBaseUrl,
logoutUrl,
intl,
isAdmin,
})}
/>
);
};
UserMenu.propTypes = {
username: PropTypes.string,
studioBaseUrl: PropTypes.string.isRequired,
logoutUrl: PropTypes.string.isRequired,
authenticatedUserAvatar: PropTypes.string,
isMobile: PropTypes.bool,
isAdmin: PropTypes.bool,
// injected
intl: intlShape.isRequired,
};
UserMenu.defaultProps = {
isMobile: false,
isAdmin: false,
authenticatedUserAvatar: null,
username: null,
};
export default injectIntl(UserMenu);

View File

@@ -1,64 +0,0 @@
// This SCSS was partly copied from edx/frontend-app-support-tools/src/support-header/index.scss.
$spacer: 1rem;
$white: #FFFFFF;
.btn-tertiary:hover {
color: white;
background-color: #00262B;
}
.course-title-lockup {
@media only screen and (max-width: 768px) {
padding-left: .5rem;
max-width: 70%;
}
@media only screen and (min-width: 769px) {
padding: .5rem;
padding-right: $spacer;
border-right: 1px solid #E5E5E5;
min-width: 70%;
}
overflow: hidden;
span {
color: #333333;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
line-height: 1.375rem;
}
}
.site-header-mobile,
.site-header-desktop {
position: relative;
z-index: 1000;
}
.site-header-mobile {img {
height: 1.5rem;
}
}
.site-header-desktop {
height: 3.75rem;
box-shadow: 0 1px 0 0 rgb(0 0 0 / .1);
background: $white;
.logo {
display: block;
box-sizing: content-box;
position: relative;
top: -.05em;
height: 1.75rem;
padding: $spacer 0;
margin-right: $spacer;
img {
display: block;
height: 100%;
}
}
}

View File

@@ -1,3 +0,0 @@
import StudioHeader from './StudioHeader';
export default StudioHeader;

View File

@@ -1,156 +0,0 @@
import { defineMessages } from '@edx/frontend-platform/i18n';
const messages = defineMessages({
'header.links.content': {
id: 'header.links.content',
defaultMessage: 'Content',
description: 'Label for Content menu trigger',
},
'header.links.settings': {
id: 'header.links.settings',
defaultMessage: 'Settings',
description: 'Label for Settings menu trigger',
},
'header.links.tools': {
id: 'header.links.content.tools',
defaultMessage: 'Tools',
description: 'Label for Tools menu trigger',
},
'header.links.outline': {
id: 'header.links.outline',
defaultMessage: 'Outline',
description: 'Link to Studio Outline page',
},
'header.links.updates': {
id: 'header.links.updates',
defaultMessage: 'Updates',
description: 'Link to Studio Updates page',
},
'header.links.pages': {
id: 'header.links.pages',
defaultMessage: 'Pages & Resources',
description: 'Link to Studio Pages page',
},
'header.links.filesAndUploads': {
id: 'header.links.filesAndUploads',
defaultMessage: 'Files & Uploads',
description: 'Link to Studio Files & Uploads page',
},
'header.links.textbooks': {
id: 'header.links.textbooks',
defaultMessage: 'Textbooks',
description: 'Link to Studio Textbooks page',
},
'header.links.videoUploads': {
id: 'header.links.videoUploads',
defaultMessage: 'Video Uploads',
description: 'Link to Studio Video Uploads page',
},
'header.links.scheduleAndDetails': {
id: 'header.links.scheduleAndDetails',
defaultMessage: 'Schedule & Details',
description: 'Link to Studio Schedule & Details page',
},
'header.links.grading': {
id: 'header.links.grading',
defaultMessage: 'Grading',
description: 'Link to Studio Grading page',
},
'header.links.courseTeam': {
id: 'header.links.courseTeam',
defaultMessage: 'Course Team',
description: 'Link to Studio Course Team page',
},
'header.links.groupConfigurations': {
id: 'header.links.groupConfigurations',
defaultMessage: 'Group Configurations',
description: 'Link to Studio Group Configurations page',
},
'header.links.proctoredExamSettings': {
id: 'header.links.proctoredExamSettings',
defaultMessage: 'Proctored Exam Settings',
description: 'Link to Studio Proctored Exam Settings page',
},
'header.links.advancedSettings': {
id: 'header.links.advancedSettings',
defaultMessage: 'Advanced Settings',
description: 'Link to Studio Advanced Settings page',
},
'header.links.certificates': {
id: 'header.links.certificates',
defaultMessage: 'Certificates',
description: 'Link to Studio Certificates page',
},
'header.links.publisher': {
id: 'header.links.publisher',
defaultMessage: 'Publisher',
description: 'Link to Publisher',
},
'header.links.import': {
id: 'header.links.import',
defaultMessage: 'Import',
description: 'Link to Studio Import page',
},
'header.links.export': {
id: 'header.links.export',
defaultMessage: 'Export',
description: 'Link to Studio Export page',
},
'header.links.checklists': {
id: 'header.links.checklists',
defaultMessage: 'Checklists',
description: 'Link to Studio Checklists page',
},
'header.user.menu.studio': {
id: 'header.user.menu.studio',
defaultMessage: 'Studio Home',
description: 'Link to Studio Home',
},
'header.user.menu.maintenance': {
id: 'header.user.menu.maintenance',
defaultMessage: 'Maintenance',
description: 'Link to the Studio maintenance page',
},
'header.user.menu.logout': {
id: 'header.user.menu.logout',
defaultMessage: 'Logout',
description: 'Logout link',
},
'header.label.account.menu': {
id: 'header.label.account.menu',
defaultMessage: 'Account Menu',
description: 'The aria label for the account menu trigger',
},
'header.label.account.menu.for': {
id: 'header.label.account.menu.for',
defaultMessage: 'Account menu for {username}',
description: 'The aria label for the account menu trigger when the username is displayed in it',
},
'header.label.main.nav': {
id: 'header.label.main.nav',
defaultMessage: 'Main',
description: 'The aria label for the main menu nav',
},
'header.label.main.menu': {
id: 'header.label.main.menu',
defaultMessage: 'Main Menu',
description: 'The aria label for the main menu trigger',
},
'header.label.main.header': {
id: 'header.label.main.header',
defaultMessage: 'Main',
description: 'The aria label for the main header',
},
'header.label.secondary.nav': {
id: 'header.label.secondary.nav',
defaultMessage: 'Secondary',
description: 'The aria label for the seconary nav',
},
'header.label.courseOutline': {
id: 'header.label.courseOutline',
defaultMessage: 'Back to course outline in Studio',
description: 'The aria label for the link back to the Studio Course Outline',
},
});
export default messages;

View File

@@ -1,36 +0,0 @@
import messages from './messages';
const getUserMenuItems = ({
studioBaseUrl,
logoutUrl,
intl,
isAdmin,
}) => {
let items = [
{
href: `${studioBaseUrl}}`,
title: intl.formatMessage(messages['header.user.menu.studio']),
}, {
href: `${logoutUrl}`,
title: intl.formatMessage(messages['header.user.menu.logout']),
},
];
if (isAdmin) {
items = [
{
href: `${studioBaseUrl}}`,
title: intl.formatMessage(messages['header.user.menu.studio']),
}, {
href: `${studioBaseUrl}/maintenance`,
title: intl.formatMessage(messages['header.user.menu.maintenance']),
}, {
href: `${logoutUrl}`,
title: intl.formatMessage(messages['header.user.menu.logout']),
},
];
}
return items;
};
export default getUserMenuItems;

View File

@@ -1,6 +0,0 @@
const executeThunk = async (thunk, dispatch, getState) => {
await thunk(dispatch, getState);
await new Promise(setImmediate);
};
export default executeThunk;