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
71 changed files with 32442 additions and 13365 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

@@ -16,4 +16,4 @@ jobs:
secrets: secrets:
GITHUB_APP_ID: ${{ secrets.GRAPHQL_AUTH_APP_ID }} GITHUB_APP_ID: ${{ secrets.GRAPHQL_AUTH_APP_ID }}
GITHUB_APP_PRIVATE_KEY: ${{ secrets.GRAPHQL_AUTH_APP_PEM }} GITHUB_APP_PRIVATE_KEY: ${{ secrets.GRAPHQL_AUTH_APP_PEM }}
SLACK_BOT_TOKEN: ${{ secrets.SLACK_ISSUE_BOT_TOKEN }} SLACK_BOT_TOKEN: ${{ secrets.SLACK_ISSUE_BOT_TOKEN }}

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@v4 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@v4 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,41 +2,38 @@ name: Release CI
on: on:
push: push:
branches: branches:
- master - master
- alpha
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@v4 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@v4 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,8 +1,5 @@
{ {
"branches": [ "branch": "master",
"master",
{name: "alpha", prerelease: true}
],
"tagFormat": "v${version}", "tagFormat": "v${version}",
"verifyConditions": [ "verifyConditions": [
"@semantic-release/npm", "@semantic-release/npm",

View File

@@ -1,12 +1,14 @@
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-formatjs transifex_temp = ./temp/babel-plugin-react-intl
build: build:
rm -rf ./dist rm -rf ./dist
@@ -17,7 +19,7 @@ build:
@rm -rf dist/__mocks__ @rm -rf dist/__mocks__
requirements: requirements:
npm ci npm install
i18n.extract: i18n.extract:
# Pulling display strings from .jsx files into .json files... # Pulling display strings from .jsx files into .json files...
@@ -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

@@ -2,42 +2,22 @@
frontend-component-header frontend-component-header
######################### #########################
|license| |Build Status| |Codecov| |npm_version| |npm_downloads| |semantic-release| |Build Status| |Codecov| |npm_version| |npm_downloads| |license| |semantic-release|
******** ********
Purpose Overview
******** ********
A generic header for Open edX micro-frontend applications. A generic header for Open edX micro-frontend applications.
************ ************
Getting Started Requirements
************ ************
Prerequisites 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>`_
=============
The `devstack`_ is currently recommended as a development environment for your
new MFE. If you start it with ``make dev.up.lms`` that should give you
everything you need as a companion to this frontend.
Note that it is also possible to use `Tutor`_ to develop an MFE. You can refer
to the `relevant tutor-mfe documentation`_ to get started using it.
.. _Devstack: https://github.com/openedx/devstack
.. _Tutor: https://github.com/overhangio/tutor
.. _relevant tutor-mfe documentation: https://github.com/overhangio/tutor-mfe#mfe-development
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>`_
Environment Variables Environment Variables
==================== =====================
* ``LMS_BASE_URL`` - The URL of the LMS of your Open edX instance. * ``LMS_BASE_URL`` - The URL of the LMS of your Open edX instance.
* ``LOGOUT_URL`` - The URL of the API endpoint which performs a user logout. * ``LOGOUT_URL`` - The URL of the API endpoint which performs a user logout.
@@ -46,14 +26,13 @@ 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.
************
Installation Installation
============ ************
To install this header into your Open edX micro-frontend, run the following command in your MFE: To install this header into your Open edX micro-frontend, run the following command in your MFE:
@@ -61,33 +40,9 @@ To install this header into your Open edX micro-frontend, run the following comm
This will make the component available to be imported into your application. This will make the component available to be imported into your application.
Cloning and Startup *****
===================
.. code-block::
1. Clone your new repo:
``git clone https://github.com/openedx/frontend-component-header.git``
2. Use node v18.x.
The current version of the micro-frontend build scripts support node 18.
Using other major versions of node *may* work, but this is unsupported. For
convenience, this repository includes an .nvmrc file to help in setting the
correct node version via `nvm <https://github.com/nvm-sh/nvm>`_.
3. Install npm dependencies:
``cd frontend-component-header && npm ci``
4. Start the dev server:
``npm start``
Usage Usage
===== *****
This library has the following exports: This library has the following exports:
@@ -98,15 +53,17 @@ 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>`_
***********
Development Development
=========== ***********
Install dependencies:: Install dependencies::
npm ci npm i
Start the development server:: Start the development server::
@@ -116,63 +73,6 @@ Build a production distribution::
npm run build npm run build
License
=======
The code in this repository is licensed under the AGPLv3 unless otherwise
noted.
Please see `LICENSE <LICENSE>`_ for details.
Contributing
============
Contributions are very welcome. Please read `How To Contribute`_ for details.
.. _How To Contribute: https://openedx.org/r/how-to-contribute
This project is currently accepting all types of contributions, bug fixes,
security fixes, maintenance work, or new features. However, please make sure
to have a discussion about your new feature idea with the maintainers prior to
beginning development to maximize the chances of your change being accepted.
You can start a conversation by creating a new issue on this repo summarizing
your idea.
Getting Help
===========
If you're having trouble, we have discussion forums at
https://discuss.openedx.org where you can connect with others in the community.
Our real-time conversations are on Slack. You can request a `Slack
invitation`_, then join our `community Slack workspace`_. Because this is a
frontend repository, the best place to discuss it would be in the `#wg-frontend
channel`_.
For anything non-trivial, the best path is to open an issue in this repository
with as many details about the issue you are facing as you can provide.
https://github.com/openedx/frontend-component-header/issues
For more information about these options, see the `Getting Help`_ page.
.. _Slack invitation: https://openedx.org/slack
.. _community Slack workspace: https://openedx.slack.com/
.. _#wg-frontend channel: https://openedx.slack.com/archives/C04BM6YC7A6
.. _Getting Help: https://openedx.org/community/connect
The Open edX Code of Conduct
============================
All community members are expected to follow the `Open edX Code of Conduct`_.
.. _Open edX Code of Conduct: https://openedx.org/code-of-conduct/
Reporting Security Issues
=========================
Please do not report security issues in public. Please email security@openedx.org.
.. |Build Status| image:: https://api.travis-ci.com/edx/frontend-component-header.svg?branch=master .. |Build Status| image:: https://api.travis-ci.com/edx/frontend-component-header.svg?branch=master
:target: https://travis-ci.com/edx/frontend-component-header :target: https://travis-ci.com/edx/frontend-component-header
.. |Codecov| image:: https://img.shields.io/codecov/c/github/edx/frontend-component-header .. |Codecov| image:: https://img.shields.io/codecov/c/github/edx/frontend-component-header
@@ -184,4 +84,4 @@ Please do not report security issues in public. Please email security@openedx.or
.. |license| image:: https://img.shields.io/npm/l/@edx/frontend-component-header.svg .. |license| image:: https://img.shields.io/npm/l/@edx/frontend-component-header.svg
:target: @edx/frontend-component-header :target: @edx/frontend-component-header
.. |semantic-release| image:: https://img.shields.io/badge/%20%20%F0%9F%93%A6%F0%9F%9A%80-semantic--release-e10079.svg .. |semantic-release| image:: https://img.shields.io/badge/%20%20%F0%9F%93%A6%F0%9F%9A%80-semantic--release-e10079.svg
:target: https://github.com/semantic-release/semantic-release :target: https://github.com/semantic-release/semantic-release

View File

@@ -1,3 +1,3 @@
const { createConfig } = require('@openedx/frontend-build'); const { createConfig } = require('@edx/frontend-build');
module.exports = createConfig('babel-preserve-modules'); module.exports = createConfig('babel-preserve-modules');

View File

@@ -7,7 +7,6 @@ import { AppContext, AppProvider } from '@edx/frontend-platform/react';
import Header from '@edx/frontend-component-header'; import Header from '@edx/frontend-component-header';
import './index.scss'; import './index.scss';
import StudioHeader from '../src/studio-header/StudioHeader';
subscribe(APP_READY, () => { subscribe(APP_READY, () => {
ReactDOM.render( ReactDOM.render(
@@ -33,35 +32,7 @@ subscribe(APP_READY, () => {
}}> }}>
<Header /> <Header />
</AppContext.Provider> </AppContext.Provider>
<h5 className="mt-2 mb-5">Logged in state</h5> <h5 className="mt-2">Logged in state</h5>
<AppContext.Provider value={{
authenticatedUser: {
userId: '123abc',
username: 'testuser',
roles: [],
administrator: false,
},
config: getConfig(),
}}>
<StudioHeader
number="run123"
org="testX"
title="Course Name"
isHiddenMainMenu={false}
mainMenuDropdowns={[
{
id: 'content-dropdown',
buttonTitle: 'Content',
items: [{
href: '#',
title: 'Outline',
}],
},
]}
outlineLink="#"
/>
</AppContext.Provider>
<h5 className="mt-2">Logged in state for Studio header</h5>
</AppProvider>, </AppProvider>,
document.getElementById('root'), document.getElementById('root'),
); );

View File

@@ -1,6 +1,6 @@
@import "@edx/brand/paragon/fonts"; @import "@edx/brand/paragon/fonts";
@import "@edx/brand/paragon/variables"; @import "@edx/brand/paragon/variables";
@import "@openedx/paragon/scss/core/core"; @import "@edx/paragon/scss/core/core";
@import "@edx/brand/paragon/overrides"; @import "@edx/brand/paragon/overrides";
@import "@edx/frontend-component-header/index"; @import "@edx/frontend-component-header/index";

View File

@@ -1,4 +1,4 @@
const { createConfig } = require('@openedx/frontend-build'); const { createConfig } = require('@edx/frontend-build');
module.exports = createConfig('jest', { module.exports = createConfig('jest', {
setupFilesAfterEnv: [ setupFilesAfterEnv: [

43102
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -8,7 +8,7 @@
}, },
"scripts": { "scripts": {
"build": "make build", "build": "make build",
"i18n_extract": "fedx-scripts formatjs extract", "i18n_extract": "BABEL_ENV=i18n fedx-scripts babel src --quiet > /dev/null",
"lint": "fedx-scripts eslint --ext .js --ext .jsx .", "lint": "fedx-scripts eslint --ext .js --ext .jsx .",
"snapshot": "fedx-scripts jest --updateSnapshot", "snapshot": "fedx-scripts jest --updateSnapshot",
"start": "fedx-scripts webpack-dev-server --progress", "start": "fedx-scripts webpack-dev-server --progress",
@@ -24,52 +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:@openedx/brand-openedx@^1.2.2", "@edx/brand": "npm:@edx/brand-openedx@1.1.0",
"@edx/browserslist-config": "^1.1.1", "@edx/frontend-build": "9.2.2",
"@edx/frontend-platform": "6.2.0", "@edx/frontend-platform": "1.15.1",
"@edx/reactifex": "^2.1.1", "@edx/paragon": "19.20.0",
"@openedx/frontend-build": "13.0.27", "codecov": "3.8.3",
"@openedx/paragon": "22.0.0", "enzyme": "3.11.0",
"@testing-library/dom": "9.3.4", "enzyme-adapter-react-16": "1.15.6",
"@testing-library/jest-dom": "5.17.0", "husky": "7.0.4",
"@testing-library/react": "10.4.9",
"husky": "8.0.3",
"jest": "29.7.0",
"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.21.3", "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.3.0" "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": {
"@fortawesome/fontawesome-svg-core": "6.5.1",
"@fortawesome/free-brands-svg-icons": "6.5.1",
"@fortawesome/free-regular-svg-icons": "6.5.1",
"@fortawesome/free-solid-svg-icons": "6.5.1",
"@fortawesome/react-fontawesome": "^0.2.0",
"axios-mock-adapter": "1.22.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.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": "^7.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"
"@openedx/paragon": ">= 21.5.7 < 23.0.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,
@@ -14,13 +16,14 @@ MenuTrigger.defaultProps = {
tag: 'div', tag: 'div',
className: null, className: null,
}; };
const MenuTriggerComp = <MenuTrigger />; const MenuTriggerType = <MenuTrigger />.type;
const MenuTriggerType = MenuTriggerComp.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,36 +1,33 @@
{ {
"header.links.courses": "المساقات",
"header.links.programs": "البرامج",
"header.links.content.search": "اكتشف الجديد",
"header.links.schools": "المدارس و الشركاء",
"header.user.menu.dashboard": "لوحة المعلومات",
"header.user.menu.profile": "الملف الشخصي",
"header.user.menu.account.settings": "الحساب",
"header.user.menu.order.history": "سجل الطلبيات",
"header.user.menu.logout": "تسجيل الخروج",
"header.user.menu.login": "تسجيل الدخول",
"header.user.menu.register": "التسجيل",
"header.user.menu.studio.home": "صفحة الاستوديو الرئيسية",
"header.user.menu.studio.maintenance": "الصيانة",
"header.label.account.nav": "الحساب",
"header.label.account.menu": "قائمة الحساب",
"header.label.account.menu.for": "قائمة حساب المستخدم {username}",
"header.label.main.nav": "القا|مة الرئيسية",
"header.label.main.menu": "القائمة الرئيسية",
"header.label.main.header": "الرئيسية",
"header.label.secondary.nav": "القائمة الثانوية",
"header.label.skip.nav": "التخطي إلى المحتوى الرئيسي",
"header.label.app.nav": "تطبيق",
"general.register.sentenceCase": "التسجيل", "general.register.sentenceCase": "التسجيل",
"general.signIn.sentenceCase": "تسجيل الدخول", "general.signIn.sentenceCase": "تسجيل الدخول",
"header.links.courses": "مساقات",
"header.links.programs": "برامج",
"header.links.content.search": "استكشف الجديد",
"header.links.schools": "المدارس والشركاء",
"header.user.menu.dashboard": "لوحة المعلومات",
"header.user.menu.profile": "الملف الشخصي",
"header.user.menu.account.settings": "حساب",
"header.user.menu.order.history": "سجل الطلبات",
"header.user.menu.logout": "تسجيل الخروج",
"header.user.menu.login": "تسجيل الدخول",
"header.user.menu.register": "تسجيل ",
"header.user.menu.studio.home": "Studio Home",
"header.user.menu.studio.maintenance": "Maintenance",
"header.label.account.nav": "حساب",
"header.label.account.menu": "قائمة الحساب",
"header.label.account.menu.for": "قائمة الحساب للمستخدم {username}",
"header.label.main.nav": "الرئيسية",
"header.label.main.menu": "القائمة الرئيسية",
"header.label.main.header": "الرئيسية",
"header.label.secondary.nav": "فرعي",
"header.label.skip.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": "تسجيل الخروج"
"header.user.menu.studio": "صفحة الاستوديو الرئيسية",
"header.user.menu.maintenance": "الصيانة",
"header.label.courseOutline": "الرجوع إلى مخطط المساق الكلّي في الاستوديو"
} }

View File

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

View File

@@ -1,36 +0,0 @@
{
"header.links.courses": "Kurse",
"header.links.programs": "Programme",
"header.links.content.search": "",
"header.links.schools": "",
"header.user.menu.dashboard": "Meine Kurse",
"header.user.menu.profile": "",
"header.user.menu.account.settings": "Konto",
"header.user.menu.order.history": "",
"header.user.menu.logout": "Abmelden",
"header.user.menu.login": "Login",
"header.user.menu.register": "",
"header.user.menu.studio.home": "",
"header.user.menu.studio.maintenance": "",
"header.label.account.nav": "Konto",
"header.label.account.menu": "",
"header.label.account.menu.for": "",
"header.label.main.nav": "",
"header.label.main.menu": "",
"header.label.main.header": "",
"header.label.secondary.nav": "Sekundarschule",
"header.label.skip.nav": "Springe zum Hauptthema",
"header.label.app.nav": "",
"general.register.sentenceCase": "",
"general.signIn.sentenceCase": "",
"header.menu.dashboard.label": "Meine Kurse",
"header.help.label": "Hilfe",
"header.menu.profile.label": "",
"header.menu.account.label": "Konto",
"header.menu.orderHistory.label": "",
"header.navigation.skipNavLink": "",
"header.menu.signOut.label": "Abmelden",
"header.user.menu.studio": "",
"header.user.menu.maintenance": "",
"header.label.courseOutline": ""
}

View File

@@ -1,4 +1,6 @@
{ {
"general.register.sentenceCase": "Registrarse",
"general.signIn.sentenceCase": "Iniciar sesión",
"header.links.courses": "Cursos", "header.links.courses": "Cursos",
"header.links.programs": "Programas", "header.links.programs": "Programas",
"header.links.content.search": "Encontrar nuevo", "header.links.content.search": "Encontrar nuevo",
@@ -10,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}",
@@ -20,17 +22,12 @@
"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",
"general.register.sentenceCase": "Registrarse",
"general.signIn.sentenceCase": "Iniciar sesión",
"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",
"header.menu.account.label": "Cuenta", "header.menu.account.label": "Cuenta",
"header.menu.orderHistory.label": "Historial de órdenes", "header.menu.orderHistory.label": "Historial de órdenes",
"header.navigation.skipNavLink": "Dirígete al contenido principal.", "header.navigation.skipNavLink": "Dirígete al contenido principal.",
"header.menu.signOut.label": "Cerrar sesión", "header.menu.signOut.label": "Cerrar sesión"
"header.user.menu.studio": "Inicio Studio",
"header.user.menu.maintenance": "Mantenimiento",
"header.label.courseOutline": "Volver al esquema del curso en Studio"
} }

View File

@@ -1,4 +1,6 @@
{ {
"general.register.sentenceCase": "S'inscrire",
"general.signIn.sentenceCase": "Connectez-vous",
"header.links.courses": "Cours", "header.links.courses": "Cours",
"header.links.programs": "Programmes", "header.links.programs": "Programmes",
"header.links.content.search": "Explorer les cours", "header.links.content.search": "Explorer les cours",
@@ -21,16 +23,11 @@
"header.label.secondary.nav": "Secondaire", "header.label.secondary.nav": "Secondaire",
"header.label.skip.nav": "Passer au contenu principal", "header.label.skip.nav": "Passer au contenu principal",
"header.label.app.nav": "Application", "header.label.app.nav": "Application",
"general.register.sentenceCase": "S'inscrire",
"general.signIn.sentenceCase": "Connectez-vous",
"header.menu.dashboard.label": "Tableau de bord", "header.menu.dashboard.label": "Tableau de bord",
"header.help.label": "Aide", "header.help.label": "Aide",
"header.menu.profile.label": "Profil", "header.menu.profile.label": "Profil",
"header.menu.account.label": "Compte", "header.menu.account.label": "Compte",
"header.menu.orderHistory.label": "Historique des commandes", "header.menu.orderHistory.label": "Historique des commandes",
"header.navigation.skipNavLink": "Passer au contenu principal", "header.navigation.skipNavLink": "Passer au contenu principal",
"header.menu.signOut.label": "Se déconnecter", "header.menu.signOut.label": "Se déconnecter"
"header.user.menu.studio": "Accueil Studio",
"header.user.menu.maintenance": "Maintenance",
"header.label.courseOutline": "Retour au plan de cours dans Studio"
} }

View File

@@ -1,4 +1,6 @@
{ {
"general.register.sentenceCase": "Inscription",
"general.signIn.sentenceCase": "Connexion",
"header.links.courses": "Cours", "header.links.courses": "Cours",
"header.links.programs": "Programmes", "header.links.programs": "Programmes",
"header.links.content.search": "Découvrir les nouveautés", "header.links.content.search": "Découvrir les nouveautés",
@@ -21,16 +23,11 @@
"header.label.secondary.nav": "Secondaire", "header.label.secondary.nav": "Secondaire",
"header.label.skip.nav": "Passer au contenu de cette vue", "header.label.skip.nav": "Passer au contenu de cette vue",
"header.label.app.nav": "Application", "header.label.app.nav": "Application",
"general.register.sentenceCase": "Inscription",
"general.signIn.sentenceCase": "Connexion",
"header.menu.dashboard.label": "Tableau de bord", "header.menu.dashboard.label": "Tableau de bord",
"header.help.label": "Aide", "header.help.label": "Aide",
"header.menu.profile.label": "Profil", "header.menu.profile.label": "Profil",
"header.menu.account.label": "Compte", "header.menu.account.label": "Compte",
"header.menu.orderHistory.label": "Historique des commandes", "header.menu.orderHistory.label": "Historique des commandes",
"header.navigation.skipNavLink": "Passer au contenu principal.", "header.navigation.skipNavLink": "Passer au contenu principal.",
"header.menu.signOut.label": "Se déconnecter", "header.menu.signOut.label": "Se déconnecter"
"header.user.menu.studio": "Accueil Studio",
"header.user.menu.maintenance": "Entretien",
"header.label.courseOutline": "Retour au plan de cours dans Studio"
} }

View File

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

View File

@@ -1,36 +0,0 @@
{
"header.links.courses": "पाठ्यक्रम",
"header.links.programs": "कार्यक्रमों",
"header.links.content.search": "नया खोजें",
"header.links.schools": "स्कूलों और भागीदारों",
"header.user.menu.dashboard": "डैशबोर्ड",
"header.user.menu.profile": "प्रोफ़ाइल",
"header.user.menu.account.settings": "खाता",
"header.user.menu.order.history": "ऑर्डर इतिहास",
"header.user.menu.logout": "लॉग आउट",
"header.user.menu.login": "लॉगिन",
"header.user.menu.register": "साइन अप",
"header.user.menu.studio.home": "स्टूडियो होम",
"header.user.menu.studio.maintenance": "अनुरक्षण करना",
"header.label.account.nav": "खाता",
"header.label.account.menu": "खाता मेनू",
"header.label.account.menu.for": "{username} के लिए खाता मेनू",
"header.label.main.nav": "मुख्य",
"header.label.main.menu": "मुख्य मेनू",
"header.label.main.header": "मुख्य",
"header.label.secondary.nav": "माध्यमिक",
"header.label.skip.nav": "मुख्य विषयवस्तु में जाएं",
"header.label.app.nav": "ऐप",
"general.register.sentenceCase": "रजिस्टर करें",
"general.signIn.sentenceCase": "साइन इन करें",
"header.menu.dashboard.label": "डैशबोर्ड",
"header.help.label": "मदद",
"header.menu.profile.label": "प्रोफ़ाइल",
"header.menu.account.label": "खाता",
"header.menu.orderHistory.label": "ऑर्डर इतिहास",
"header.navigation.skipNavLink": "मुख्य सामग्री पर जाएँ।",
"header.menu.signOut.label": "साइन आउट करें",
"header.user.menu.studio": "स्टूडियो होम",
"header.user.menu.maintenance": "अनुरक्षण करना",
"header.label.courseOutline": "स्टूडियो में पाठ्यक्रम की रूपरेखा पर वापस जाएँ"
}

View File

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

View File

@@ -1,36 +0,0 @@
{
"header.links.courses": "Corsi",
"header.links.programs": "",
"header.links.content.search": "",
"header.links.schools": "Scuole e Partner",
"header.user.menu.dashboard": "Pannello di controllo",
"header.user.menu.profile": "Profilo",
"header.user.menu.account.settings": "Account",
"header.user.menu.order.history": "Cronologia Ordini",
"header.user.menu.logout": "",
"header.user.menu.login": "",
"header.user.menu.register": "Registrazione",
"header.user.menu.studio.home": "",
"header.user.menu.studio.maintenance": "",
"header.label.account.nav": "Account",
"header.label.account.menu": "",
"header.label.account.menu.for": "",
"header.label.main.nav": "",
"header.label.main.menu": "",
"header.label.main.header": "",
"header.label.secondary.nav": "",
"header.label.skip.nav": "Passa al contenuto principale",
"header.label.app.nav": "",
"general.register.sentenceCase": "Registrazione",
"general.signIn.sentenceCase": "Accedi",
"header.menu.dashboard.label": "Pannello di controllo",
"header.help.label": "Aiuto",
"header.menu.profile.label": "Profilo",
"header.menu.account.label": "Account",
"header.menu.orderHistory.label": "Cronologia Ordini",
"header.navigation.skipNavLink": "",
"header.menu.signOut.label": "Esci",
"header.user.menu.studio": "",
"header.user.menu.maintenance": "",
"header.label.courseOutline": ""
}

View File

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

View File

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

View File

@@ -1,36 +0,0 @@
{
"header.links.courses": "",
"header.links.programs": "",
"header.links.content.search": "",
"header.links.schools": "",
"header.user.menu.dashboard": "",
"header.user.menu.profile": "",
"header.user.menu.account.settings": "",
"header.user.menu.order.history": "",
"header.user.menu.logout": "",
"header.user.menu.login": "Login",
"header.user.menu.register": "",
"header.user.menu.studio.home": "",
"header.user.menu.studio.maintenance": "",
"header.label.account.nav": "",
"header.label.account.menu": "",
"header.label.account.menu.for": "",
"header.label.main.nav": "",
"header.label.main.menu": "",
"header.label.main.header": "",
"header.label.secondary.nav": "",
"header.label.skip.nav": "",
"header.label.app.nav": "",
"general.register.sentenceCase": "",
"general.signIn.sentenceCase": "",
"header.menu.dashboard.label": "",
"header.help.label": "",
"header.menu.profile.label": "",
"header.menu.account.label": "",
"header.menu.orderHistory.label": "",
"header.navigation.skipNavLink": "",
"header.menu.signOut.label": "",
"header.user.menu.studio": "",
"header.user.menu.maintenance": "",
"header.label.courseOutline": ""
}

View File

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

View File

@@ -1,36 +1 @@
{ {}
"header.links.courses": "Курсы",
"header.links.programs": "Программы",
"header.links.content.search": "Каталог курсов",
"header.links.schools": "Учебные заведения и партнёры",
"header.user.menu.dashboard": "Панель управления",
"header.user.menu.profile": "Профиль",
"header.user.menu.account.settings": "Учётная запись",
"header.user.menu.order.history": "История заказов",
"header.user.menu.logout": "Выйти",
"header.user.menu.login": "Войти",
"header.user.menu.register": "Зарегистрироваться",
"header.user.menu.studio.home": "Studio Дом",
"header.user.menu.studio.maintenance": "Техническое обслуживание",
"header.label.account.nav": "Учётная запись",
"header.label.account.menu": "Меню учетной записи",
"header.label.account.menu.for": "Меню учетной записи для {username}",
"header.label.main.nav": "Главный",
"header.label.main.menu": "Главное меню",
"header.label.main.header": "Главный",
"header.label.secondary.nav": "Среднее образование",
"header.label.skip.nav": "Перейти к основному содержимому",
"header.label.app.nav": "Приложение",
"general.register.sentenceCase": "Регистрация",
"general.signIn.sentenceCase": "Вход",
"header.menu.dashboard.label": "Панель управления",
"header.help.label": "Помощь",
"header.menu.profile.label": "Профиль",
"header.menu.account.label": "Учётная запись",
"header.menu.orderHistory.label": "История заказов",
"header.navigation.skipNavLink": "Перейти к контенту",
"header.menu.signOut.label": "Выйти",
"header.user.menu.studio": "Studio Дом",
"header.user.menu.maintenance": "Техническое обслуживание",
"header.label.courseOutline": ""
}

View File

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

View File

@@ -1,36 +1 @@
{ {}
"header.links.courses": "Курси",
"header.links.programs": "Програми",
"header.links.content.search": "Відкривайте Нове",
"header.links.schools": "Школи та партнери",
"header.user.menu.dashboard": "Мої курси",
"header.user.menu.profile": "Профіль",
"header.user.menu.account.settings": "Обліковий запис",
"header.user.menu.order.history": "Історія замовлень",
"header.user.menu.logout": "Вийти",
"header.user.menu.login": "Увійти",
"header.user.menu.register": "Зареєструватися",
"header.user.menu.studio.home": "Головна сторінка в Студії",
"header.user.menu.studio.maintenance": "Технічні робити",
"header.label.account.nav": "Обліковий запис",
"header.label.account.menu": "Меню облікового запису",
"header.label.account.menu.for": "Меню облікового запису для {username}",
"header.label.main.nav": "Головна",
"header.label.main.menu": "Головне меню",
"header.label.main.header": "Головна",
"header.label.secondary.nav": "Середня",
"header.label.skip.nav": "Перейти до головного змісту",
"header.label.app.nav": "Додаток",
"general.register.sentenceCase": "Зареєструватися",
"general.signIn.sentenceCase": "Увійти",
"header.menu.dashboard.label": "Мої курси",
"header.help.label": "Допомога",
"header.menu.profile.label": "Профіль",
"header.menu.account.label": "Обліковий запис",
"header.menu.orderHistory.label": "Історія замовлень",
"header.navigation.skipNavLink": "Перейти до головного змісту.",
"header.menu.signOut.label": "Вийти",
"header.user.menu.studio": "Головна сторінка в Студії",
"header.user.menu.maintenance": "Технічні робити",
"header.label.courseOutline": "Повернутися до плану курсу в Studio"
}

View File

@@ -1,36 +1,33 @@
{ {
"header.links.courses": "课程", "general.register.sentenceCase": "Register",
"header.links.programs": "项目", "general.signIn.sentenceCase": "Sign in",
"header.links.content.search": "马上探索课程", "header.links.courses": "Courses",
"header.links.schools": "学校 & 伙伴", "header.links.programs": "Programs",
"header.user.menu.dashboard": "课程面板", "header.links.content.search": "Discover New",
"header.user.menu.profile": "个人主页", "header.links.schools": "Schools & Partners",
"header.user.menu.account.settings": "账号", "header.user.menu.dashboard": "Dashboard",
"header.user.menu.order.history": "订单记录", "header.user.menu.profile": "Profile",
"header.user.menu.logout": "退出", "header.user.menu.account.settings": "Account",
"header.user.menu.login": "登录", "header.user.menu.order.history": "Order History",
"header.user.menu.register": "注册", "header.user.menu.logout": "Logout",
"header.user.menu.studio.home": "工作室主页", "header.user.menu.login": "Login",
"header.user.menu.studio.maintenance": "维护", "header.user.menu.register": "Sign Up",
"header.label.account.nav": "账号", "header.user.menu.studio.home": "Studio Home",
"header.label.account.menu": "账户菜单", "header.user.menu.studio.maintenance": "Maintenance",
"header.label.account.menu.for": "{username} 的帐户菜单", "header.label.account.nav": "Account",
"header.label.main.nav": "主要", "header.label.account.menu": "Account Menu",
"header.label.main.menu": "主菜单", "header.label.account.menu.for": "Account menu for {username}",
"header.label.main.header": "主要", "header.label.main.nav": "Main",
"header.label.secondary.nav": "高中", "header.label.main.menu": "Main Menu",
"header.label.skip.nav": "跳转到主要内容", "header.label.main.header": "Main",
"header.label.app.nav": "", "header.label.secondary.nav": "Secondary",
"general.register.sentenceCase": "注册", "header.label.skip.nav": "Skip to main content",
"general.signIn.sentenceCase": "登录", "header.label.app.nav": "App",
"header.menu.dashboard.label": "课程面板", "header.menu.dashboard.label": "Dashboard",
"header.help.label": "帮助", "header.help.label": "Help",
"header.menu.profile.label": "个人主页", "header.menu.profile.label": "Profile",
"header.menu.account.label": "账号", "header.menu.account.label": "Account",
"header.menu.orderHistory.label": "订单记录", "header.menu.orderHistory.label": "Order History",
"header.navigation.skipNavLink": "跳回主頁", "header.navigation.skipNavLink": "Skip to main content.",
"header.menu.signOut.label": "注销", "header.menu.signOut.label": "Sign Out"
"header.user.menu.studio": "工作室主页",
"header.user.menu.maintenance": "维护",
"header.label.courseOutline": "返回 Studio 中的课程大纲"
} }

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/StudioHeader.scss';
.dropdown-item a { .dropdown-item a {
text-decoration: none; text-decoration: none;
@@ -28,7 +27,7 @@ $white: #fff;
.learning-header { .learning-header {
min-width: 0; min-width: 0;
.course-title-lockup { .course-title-lockup {
min-width: 0; min-width: 0;
@@ -43,9 +42,9 @@ $white: #fff;
.user-dropdown { .user-dropdown {
.btn { .btn {
height: 3rem; height: 3rem;
// @media (max-width: -1 + map-get($grid-breakpoints, "sm")) { @media (max-width: -1 + map-get($grid-breakpoints, "sm")) {
// padding: 0 0.5rem; padding: 0 0.5rem;
// } }
} }
} }
} }

View File

@@ -3,27 +3,29 @@ import React from 'react';
import { getConfig } from '@edx/frontend-platform'; import { getConfig } from '@edx/frontend-platform';
import { getLoginRedirectUrl } from '@edx/frontend-platform/auth'; import { getLoginRedirectUrl } from '@edx/frontend-platform/auth';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n'; import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { Button } from '@openedx/paragon'; 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 '@openedx/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

@@ -1,7 +1,9 @@
/* eslint-disable import/no-extraneous-dependencies */ /* eslint-disable import/no-extraneous-dependencies */
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 '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';
@@ -15,11 +17,11 @@ import { IntlProvider } from 'react-intl';
import AppProvider from '@edx/frontend-platform/react/AppProvider'; import AppProvider from '@edx/frontend-platform/react/AppProvider';
import appMessages from './i18n'; import appMessages from './i18n';
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';
@@ -100,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 '@openedx/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 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,160 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
import {
ActionRow,
Button,
Container,
Nav,
Row,
} from '@openedx/paragon';
import { Close, MenuIcon } from '@openedx/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-2.5">
<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>
) : (
<div className="w-25">
<Row className="m-0 flex-nowrap">
{renderBrandNav}
<CourseLockUp
{...{
outlineLink,
number,
org,
title,
}}
/>
</Row>
</div>
)}
{isMobile ? (
<>
<ActionRow.Spacer />
{renderBrandNav}
</>
) : (
<Nav data-testid="desktop-menu" className="ml-2">
{mainMenuDropdowns.map(dropdown => {
const { id, buttonTitle, items } = dropdown;
return (
<NavDropdownMenu key={id} {...{ 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,
toggleModalPopup: PropTypes.func,
isModalPopupOpen: PropTypes.bool,
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 = {
setModalPopupTarget: null,
toggleModalPopup: null,
isModalPopupOpen: false,
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,73 +0,0 @@
import React, { useState } from 'react';
import PropTypes from 'prop-types';
import { useToggle, ModalPopup } from '@openedx/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,
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 '@openedx/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,40 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
import {
Dropdown,
DropdownButton,
} from '@openedx/paragon';
const NavDropdownMenu = ({
id,
buttonTitle,
items,
}) => (
<DropdownButton
id={id}
title={buttonTitle}
variant="outline-primary"
className="mr-2"
>
{items.map(item => (
<Dropdown.Item
key={`${item.title}-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,75 +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 (
<div className="studio-header">
<a className="nav-skip sr-only sr-only-focusable" href="#main">Skip to content</a>
<Responsive maxWidth={841}>
<MobileHeader {...props} />
</Responsive>
<Responsive minWidth={842}>
<HeaderBody {...props} />
</Responsive>
</div>
);
};
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,49 +0,0 @@
$spacer: 1rem;
$white: #FFFFFF;
.studio-header {
position: relative;
z-index: 1000;
height: 3.75rem;
box-shadow: 0 1px 0 0 rgb(0 0 0 / .1);
background: $white;
.btn-outline-primary {
border-color: $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%;
}
}
.course-title-lockup {
@media only screen and (min-width: 769px) {
padding: .5rem;
padding-right: $spacer;
border-right: 1px solid #E5E5E5;
width: 70%;
}
overflow: hidden;
span {
color: #333333;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
line-height: 1.375rem;
}
}
}

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 '@openedx/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,3 +0,0 @@
import StudioHeader from './StudioHeader';
export default StudioHeader;

View File

@@ -1,56 +0,0 @@
import { defineMessages } from '@edx/frontend-platform/i18n';
const messages = defineMessages({
'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;

View File

@@ -1,5 +1,5 @@
const path = require('path'); const path = require('path');
const { createConfig } = require('@openedx/frontend-build'); const { createConfig } = require('@edx/frontend-build');
module.exports = createConfig('webpack-dev', { module.exports = createConfig('webpack-dev', {
entry: path.resolve(__dirname, 'example'), entry: path.resolve(__dirname, 'example'),