Compare commits

..

1 Commits

Author SHA1 Message Date
jansenk
eae2da950e chore: remove 'enhanced' from page title 2022-05-09 16:32:02 -04:00
270 changed files with 51838 additions and 17469 deletions

3
.env
View File

@@ -30,6 +30,3 @@ ENTERPRISE_MARKETING_URL=''
ENTERPRISE_MARKETING_UTM_SOURCE=''
ENTERPRISE_MARKETING_UTM_CAMPAIGN=''
ENTERPRISE_MARKETING_FOOTER_UTM_MEDIUM=''
APP_ID=''
MFE_CONFIG_API_URL=''
ACCOUNT_SETTINGS_URL=''

View File

@@ -7,6 +7,7 @@ LOGOUT_URL='http://localhost:18000/logout'
LOGO_URL=https://edx-cdn.org/v3/default/logo.svg
LOGO_TRADEMARK_URL=https://edx-cdn.org/v3/default/logo-trademark.svg
LOGO_WHITE_URL=https://edx-cdn.org/v3/default/logo-white.svg
LOGO_POWERED_BY_OPEN_EDX_URL_SVG=https://edx-cdn.org/v3/stage/open-edx-tag.svg
FAVICON_URL=https://edx-cdn.org/v3/default/favicon.ico
CSRF_TOKEN_API_PATH='/csrf/api/v1/token'
REFRESH_ACCESS_TOKEN_ENDPOINT='http://localhost:18000/login_refresh'
@@ -35,6 +36,3 @@ ENTERPRISE_MARKETING_URL='http://example.com'
ENTERPRISE_MARKETING_UTM_SOURCE='example.com'
ENTERPRISE_MARKETING_UTM_CAMPAIGN='example.com Referral'
ENTERPRISE_MARKETING_FOOTER_UTM_MEDIUM='Footer'
APP_ID=''
MFE_CONFIG_API_URL=''
ACCOUNT_SETTINGS_URL=http://localhost:1997

View File

@@ -7,6 +7,7 @@ LOGOUT_URL='http://localhost:18000/logout'
LOGO_URL=https://edx-cdn.org/v3/default/logo.svg
LOGO_TRADEMARK_URL=https://edx-cdn.org/v3/default/logo-trademark.svg
LOGO_WHITE_URL=https://edx-cdn.org/v3/default/logo-white.svg
LOGO_POWERED_BY_OPEN_EDX_URL_SVG=https://edx-cdn.org/v3/stage/open-edx-tag.svg
FAVICON_URL=https://edx-cdn.org/v3/default/favicon.ico
CSRF_TOKEN_API_PATH='/csrf/api/v1/token'
REFRESH_ACCESS_TOKEN_ENDPOINT='http://localhost:18000/login_refresh'
@@ -35,4 +36,3 @@ ENTERPRISE_MARKETING_URL='http://example.com'
ENTERPRISE_MARKETING_UTM_SOURCE='example.com'
ENTERPRISE_MARKETING_UTM_CAMPAIGN='example.com Referral'
ENTERPRISE_MARKETING_FOOTER_UTM_MEDIUM='Footer'
ACCOUNT_SETTINGS_URL=http://localhost:1997

View File

@@ -1,18 +1,11 @@
// eslint-disable-next-line import/no-extraneous-dependencies
const { createConfig } = require('@openedx/frontend-build');
const { createConfig } = require('@edx/frontend-build');
const config = createConfig('eslint', {
rules: {
'import/no-named-as-default': 'off',
'import/no-named-as-default-member': 'off',
'import/no-import-module-exports': 'off',
'import/no-self-import': 'off',
'spaced-comment': ['error', 'always', { 'block': { 'exceptions': ['*'] } }],
'react-hooks/rules-of-hooks': 'off',
"react/forbid-prop-types": ["error", { "forbid": ["any", "array"] }], // arguable object proptype is use when I do not care about the shape of the object
'no-import-assign': 'off',
'no-promise-executor-return': 'off',
'import/no-cycle': 'off',
},
});

6
.github/CODEOWNERS vendored
View File

@@ -1,6 +0,0 @@
# Code owners for frontend-app-ora-grading
# These owners will be the default owners for everything in
# the repo. Unless a later match takes precedence, they will
# be requested for review when someone opens a pull request.
* @edx/content-aurora

View File

@@ -1,19 +0,0 @@
# Run the workflow that adds new tickets that are either:
# - labelled "DEPR"
# - title starts with "[DEPR]"
# - body starts with "Proposal Date" (this is the first template field)
# to the org-wide DEPR project board
name: Add newly created DEPR issues to the DEPR project board
on:
issues:
types: [opened]
jobs:
routeissue:
uses: openedx/.github/.github/workflows/add-depr-ticket-to-depr-board.yml@master
secrets:
GITHUB_APP_ID: ${{ secrets.GRAPHQL_AUTH_APP_ID }}
GITHUB_APP_PRIVATE_KEY: ${{ secrets.GRAPHQL_AUTH_APP_PEM }}
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

@@ -10,19 +10,16 @@ on:
jobs:
tests:
runs-on: ubuntu-latest
runs-on: ubuntu-20.04
strategy:
matrix:
node: [18.15, 20]
node: [12, 14, 16]
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Setup Nodejs Env
run: echo "NODE_VER=`cat .nvmrc`" >> $GITHUB_ENV
uses: actions/checkout@v2
- name: Setup Nodejs
uses: actions/setup-node@v4
# Because of node 18 bug (https://github.com/nodejs/node/issues/47563), Pinning node version 18.15 until the next release of node
uses: actions/setup-node@v2
with:
node-version: ${{ matrix.node }}
@@ -42,10 +39,7 @@ jobs:
run: npm run build
- name: Run Coverage
uses: codecov/codecov-action@v4
with:
token: ${{ secrets.CODECOV_TOKEN }}
fail_ci_if_error: true
uses: codecov/codecov-action@v2
- name: Send failure notification
if: ${{ failure() }}

View File

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

View File

@@ -1,13 +0,0 @@
#check package-lock file version
name: Lockfile Version check
on:
push:
branches:
- master
pull_request:
jobs:
version-check:
uses: openedx/.github/.github/workflows/lockfileversion-check-v3.yml@master

View File

@@ -7,7 +7,7 @@ on:
jobs:
release:
name: Release
runs-on: ubuntu-latest
runs-on: ubuntu-20.04
steps:
- name: Checkout
uses: actions/checkout@v2

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

@@ -25,5 +25,3 @@ module.config.js
### transifex ###
src/i18n/transifex_input.json
temp
src/i18n/messages

1
.nvmrc
View File

@@ -1 +0,0 @@
20

9
.tx/config Normal file
View File

@@ -0,0 +1,9 @@
[main]
host = https://www.transifex.com
[o:open-edx:p:edx-platform:r:frontend-app-ora-grading]
file_filter = src/i18n/messages/<lang>.json
source_file = src/i18n/transifex_input.json
source_lang = en
type = KEYVALUEJSON

View File

@@ -2,15 +2,19 @@ npm-install-%: ## install specified % npm package
npm install $* --save-dev
git add package.json
intl_imports = ./node_modules/.bin/intl-imports.js
transifex_resource = frontend-app-ora-grading
transifex_langs = "ar,fr,es_419,zh_CN"
transifex_utils = ./node_modules/.bin/transifex-utils.js
i18n = ./src/i18n
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 .
transifex_temp = ./temp/babel-plugin-formatjs
transifex_temp = ./temp/babel-plugin-react-intl
NPM_TESTS=build i18n_extract lint test
NPM_TESTS=build i18n_extract lint test is-es5
.PHONY: test
test: $(addprefix test.npm.,$(NPM_TESTS)) ## validate ci suite
@@ -40,18 +44,20 @@ detect_changed_source_translations:
# Checking for changed translations...
git diff --exit-code $(i18n)
pull_translations:
rm -rf src/i18n/messages
mkdir src/i18n/messages
cd src/i18n/messages \
&& atlas pull $(ATLAS_OPTIONS) \
translations/frontend-component-footer/src/i18n/messages:frontend-component-footer \
translations/frontend-component-header/src/i18n/messages:frontend-component-header \
translations/frontend-platform/src/i18n/messages:frontend-platform \
translations/paragon/src/i18n/messages:paragon \
translations/frontend-app-ora-grading/src/i18n/messages:frontend-app-ora-grading
# Pushes translations to Transifex. You must run make extract_translations first.
push_translations:
# Pushing strings to Transifex...
tx push -s
# Fetching hashes from Transifex...
./node_modules/reactifex/bash_scripts/get_hashed_strings.sh $(tx_url1)
# Writing out comments to file...
$(transifex_utils) $(transifex_temp) --comments
# Pushing comments to Transifex...
./node_modules/reactifex/bash_scripts/put_comments.sh $(tx_url2)
$(intl_imports) frontend-component-footer frontend-component-header frontend-platform paragon frontend-app-ora-grading
# Pulls translations from Transifex.
pull_translations:
tx pull -f --mode reviewed --languages=$(transifex_langs)
# This target is used by CI.
validate-no-uncommitted-package-lock-changes:

View File

@@ -1,239 +0,0 @@
frontend-app-ora-grading
#############################
|license-badge| |status-badge| |ci-badge| |codecov-badge|
Purpose
*******
The ORA Staff Grading App is a micro-frontend (MFE) staff grading experience
for Open Response Assessments (ORAs). This experience was designed to
streamline the grading process and enable richer previews of submission content
and, eventually, replace on-platform grading workflows for ORA.
When enabled, ORAs with a staff grading step will link to this new MFE when
clicking "Grade Available Responses" from the ORA or link in the instructor
dashboard.
The ORA Staff Grader depends on the `lms/djangoapps/ora_staff_grader
<https://github.com/openedx/edx-platform/tree/master/lms/djangoapps/ora_staff_grader>`_
app in ``edx-platform``.
Getting Started
***************
Prerequisites
=============
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
Plugins
=======
This MFE can be customized using `Frontend Plugin Framework <https://github.com/openedx/frontend-plugin-framework>`_.
The parts of this MFE that can be customized in that manner are documented `here </src/plugin-slots>`_.
Developing
==========
Cloning and Startup
--------------
First, clone the repo, install code prerequisites, and start the app.
.. code-block::
1. Clone your new repo:
``git clone git@github.com:openedx/frontend-app-ora-grading.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-app-ora-grading && npm install``
4. Update the application port to use for local development:
Default port is 1993. If this does not work for you, update the line
`PORT=1993` to your port in all .env.* files
5. Start the dev server:
``npm start``
The app will, by default, run on `http://localhost:1993` unless otherwise
specified in ``.env.development:PORT`` and ``.env.development:BASE_URL``.
Next, enable the ORA Grading micro-frontend in `edx-platform`
#. Add the path to the ORA Grading app in `edx-platform`:
#. Go to your environment settings (e.g. `edx-platform/lms/envs/private.py`)
#. Add the environment variable, ``ORA_GRADING_MICROFRONTEND_URL`` pointing
to the ORA Grading app location (e.g. ``http://localhost:1993``).
#. Start / restart the ``edx-platform`` ``lms``.
#. Enable the ORA Grading feature in Django Admin.
#. Go to Django Admin (`{lms-root}/admin`)
#. Navigate to ``django-waffle`` > ``Flags`` and click ``add/enable a new
flag``.
#. Add a new flag called ``openresponseassessment.enhanced_staff_grader``
and enable it.
From there, visit an Open Response Assessment with a Staff Graded Step and
click the "View and grade responses" button to begin grading in the ORA Staff
Grader experience.
Making Changes
--------------
Get / install the latest code:
.. code-block::
# Grab the latest code
git checkout master
git pull
# Install/update the dev requirements
npm install
Before committing:
.. code-block::
# Make a new branch for your changes
git checkout -b <your_github_username>/<short_description>
# Using your favorite editor, edit the code to make your change.
# Run your new tests
npm test
# Commit all your changes
git commit ...
git push
# Open a PR and ask for review.
Deploying
=========
This component follows the standard deploy process for MFEs. For details, see
the `MFE production deployment guide`_
.. _MFE production deployment guide: https://openedx.github.io/frontend-platform/#production-deployment-strategy
Internationalization
====================
Please see refer to the `frontend-platform i18n howto`_ for documentation on
internationalization.
.. _frontend-platform i18n howto: https://github.com/openedx/frontend-platform/blob/master/docs/how_tos/i18n.rst
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-app-ora-grading/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
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.
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/
People
******
The assigned maintainers for this component and other project details may be
found in `Backstage`_. Backstage pulls this data from the ``catalog-info.yaml``
file in this repo.
.. _Backstage: https://open-edx-backstage.herokuapp.com/catalog/default/component/frontend-app-ora-grading
Reporting Security Issues
*************************
Please do not report security issues in public, and email security@openedx.org instead.
.. |license-badge| image:: https://img.shields.io/github/license/openedx/frontend-app-ora-grading.svg
:target: https://github.com/openedx/frontend-app-ora-grading/blob/master/LICENSE
:alt: License
.. |status-badge| image:: https://img.shields.io/badge/Status-Maintained-brightgreen
.. |ci-badge| image:: https://github.com/openedx/frontend-app-ora-grading/actions/workflows/ci.yml/badge.svg
:target: https://github.com/openedx/frontend-app-ora-grading/actions/workflows/ci.yml
:alt: Continuous Integration
.. |codecov-badge| image:: https://codecov.io/github/openedx/frontend-app-ora-grading/coverage.svg?branch=master
:target: https://codecov.io/github/openedx/frontend-app-ora-grading?branch=master
:alt: Codecov

View File

@@ -1,19 +0,0 @@
# This file records information about this repo. Its use is described in OEP-55:
# https://open-edx-proposals.readthedocs.io/en/latest/processes/oep-0055-proc-project-maintainers.html
apiVersion: backstage.io/v1alpha1
kind: Component
metadata:
name: 'frontend-app-ora-grading'
description: "Frontend grading experience for Open Response Assessments (ORAs)"
links:
- url: "https://ora-grading.edx.org"
title: "Production Site"
icon: "Web"
- url: "https://ora-grading.stage.edx.org"
title: "Stage Site"
icon: "Web"
spec:
owner: "group:openedx-unmaintained"
type: 'website'
lifecycle: 'production'

View File

@@ -26,3 +26,4 @@ There are only two requirements for a good `make target` name
What `make validate-no-uncommitted-package-lock-changes` does is `git diff`s for any `package-lock.json` file changes in your project.
This is important because `npm` uses the pinned dependencies in your `package-lock.json` file to build the `node_modules` directory. However, the dependencies defined within the `package.json` file can be modified manually, for example, to become misaligned with the dependencies defined within the `package-lock.json`. So when `npm install` executes, the `package-lock.json` file will be updated to mirror the modified `package.json` changes.

View File

@@ -1,4 +1,4 @@
const { createConfig } = require('@openedx/frontend-build');
const { createConfig } = require('@edx/frontend-build');
module.exports = createConfig('jest', {
setupFilesAfterEnv: [
@@ -6,12 +6,12 @@ module.exports = createConfig('jest', {
'<rootDir>/src/setupTest.js',
],
modulePaths: ['<rootDir>/src/'],
snapshotSerializers: [
'enzyme-to-json/serializer',
],
coveragePathIgnorePatterns: [
'src/segment.js',
'src/postcss.config.js',
'testUtils', // don't unit test jest mocking tools
'src/data/services/lms/fakeData', // don't unit test mock data
'src/test', // don't unit test integration test utils
],
testTimeout: 120000,
testEnvironment: 'jsdom',

57949
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -6,12 +6,11 @@
"type": "git",
"url": "git+https://github.com/edx/frontend-app-ora-grading.git"
},
"browserslist": [
"extends @edx/browserslist-config"
],
"scripts": {
"build": "fedx-scripts webpack",
"i18n_extract": "fedx-scripts formatjs extract",
"coveralls": "cat ./coverage/lcov.info | coveralls",
"is-es5": "es-check es5 ./dist/*.js",
"i18n_extract": "BABEL_ENV=i18n fedx-scripts babel src --quiet > /dev/null",
"lint": "fedx-scripts eslint --ext .jsx,.js src/",
"lint-fix": "fedx-scripts eslint --fix --ext .jsx,.js src/",
"semantic-release": "semantic-release",
@@ -27,71 +26,68 @@
"access": "public"
},
"dependencies": {
"@edx/brand": "npm:@openedx/brand-openedx@^1.2.2",
"@edx/frontend-component-header": "^5.6.0",
"@edx/frontend-platform": "8.0.0",
"@edx/openedx-atlas": "^0.6.0",
"@edx/brand": "npm:@edx/brand-edx.org@^2.0.3",
"@edx/frontend-component-footer": "10.1.6",
"@edx/frontend-platform": "^1.15.6",
"@edx/paragon": "16.14.4",
"@fortawesome/fontawesome-svg-core": "^1.2.36",
"@fortawesome/free-brands-svg-icons": "^5.15.4",
"@fortawesome/free-solid-svg-icons": "^5.15.4",
"@fortawesome/react-fontawesome": "^0.2.0",
"@openedx/frontend-slot-footer": "^1.0.2",
"@openedx/paragon": "21.11.3",
"@fortawesome/react-fontawesome": "^0.1.15",
"@redux-beacon/segment": "^1.1.0",
"@reduxjs/toolkit": "^1.6.1",
"@testing-library/user-event": "^14.0.0",
"@testing-library/user-event": "^13.5.0",
"@zip.js/zip.js": "^2.4.6",
"axios": "^0.28.0",
"axios": "^0.21.4",
"classnames": "^2.3.1",
"core-js": "3.35.1",
"core-js": "3.16.2",
"dompurify": "^2.3.1",
"email-prop-type": "^3.0.1",
"enzyme": "^3.11.0",
"enzyme-to-json": "^3.6.2",
"file-saver": "^2.0.5",
"filesize": "^8.0.6",
"font-awesome": "4.7.0",
"history": "5.3.0",
"history": "5.0.1",
"html-react-parser": "^1.3.0",
"lodash": "^4.17.21",
"moment": "^2.29.3",
"prop-types": "15.8.1",
"query-string": "7.1.3",
"react": "^17.0.2",
"react-dom": "^17.0.2",
"react-helmet": "^6.1.0",
"react-intl": "6.4.7",
"prop-types": "15.7.2",
"query-string": "7.0.1",
"react": "^16.14.0",
"react-dom": "^16.14.0",
"react-intl": "^5.20.9",
"react-pdf": "^5.5.0",
"react-redux": "^7.2.9",
"react-router": "6.21.3",
"react-router-dom": "6.21.3",
"react-redux": "^7.2.4",
"react-router": "5.2.0",
"react-router-dom": "5.2.0",
"react-router-redux": "^5.0.0-alpha.9",
"redux": "4.2.1",
"redux": "4.1.1",
"redux-beacon": "^2.1.0",
"redux-devtools-extension": "2.13.9",
"redux-logger": "3.0.6",
"redux-thunk": "2.4.2",
"regenerator-runtime": "^0.14.0",
"redux-thunk": "2.3.0",
"regenerator-runtime": "^0.13.9",
"reselect": "^4.0.0",
"util": "^0.12.4",
"whatwg-fetch": "^3.6.2"
},
"devDependencies": {
"@edx/browserslist-config": "^1.3.0",
"@edx/react-unit-test-utils": "3.0.0",
"@edx/reactifex": "^2.1.1",
"@openedx/frontend-build": "14.0.3",
"@testing-library/jest-dom": "^6.0.0",
"@testing-library/react": "12.1.5",
"@edx/frontend-build": "^9.1.4",
"@testing-library/jest-dom": "^5.14.1",
"@testing-library/react": "^12.1.0",
"axios-mock-adapter": "^1.20.0",
"codecov": "^3.8.3",
"enzyme-adapter-react-16": "^1.15.6",
"es-check": "^6.0.0",
"fetch-mock": "^9.11.0",
"husky": "^7.0.0",
"identity-obj-proxy": "^3.0.0",
"jest": "29.7.0",
"jest-environment-jsdom": "^29.7.0",
"jest": "27.0.6",
"jest-expect-message": "^1.0.2",
"react-dev-utils": "^12.0.1",
"react-test-renderer": "^17.0.2",
"react-dev-utils": "^11.0.4",
"react-test-renderer": "^16.14.0",
"reactifex": "1.1.1",
"redux-mock-store": "^1.5.4",
"semantic-release": "^19.0.3"
"semantic-release": "^17.4.5"
}
}

View File

@@ -1,7 +1,7 @@
<!doctype html>
<html lang="en-us" dir="ltr">
<head>
<title>ORA staff grading | <%= process.env.SITE_NAME %></title>
<title>ORA Staff Grader | <%= process.env.SITE_NAME %></title>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="shortcut icon" href="<%=htmlWebpackPlugin.options.FAVICON_URL%>" type="image/x-icon" />

View File

@@ -1,34 +0,0 @@
{
"extends": [
"config:base",
"schedule:weekly",
":automergeLinters",
":automergeMinor",
":automergeTesters",
":enableVulnerabilityAlerts",
":rebaseStalePrs",
":semanticCommits",
":updateNotScheduled"
],
"packageRules": [
{
"matchDepTypes": [
"devDependencies"
],
"matchUpdateTypes": [
"lockFileMaintenance",
"minor",
"patch",
"pin"
],
"automerge": true
},
{
"matchPackagePatterns": ["@edx", "@openedx"],
"matchUpdateTypes": ["minor", "patch"],
"automerge": true
}
],
"timezone": "America/New_York",
"schedule": ["before 11pm"]
}

View File

@@ -3,36 +3,29 @@ import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { BrowserRouter as Router } from 'react-router-dom';
import FooterSlot from '@openedx/frontend-slot-footer';
import { LearningHeader as Header } from '@edx/frontend-component-header';
import Footer from '@edx/frontend-component-footer';
import { selectors } from 'data/redux';
import DemoWarning from 'containers/DemoWarning';
import CTA from 'containers/CTA';
import NotificationsBanner from 'containers/NotificationsBanner';
import CourseHeader from 'containers/CourseHeader';
import ListView from 'containers/ListView';
import './App.scss';
import Head from './components/Head';
export const App = ({ courseMetadata, isEnabled }) => (
<Router>
<div>
<Head />
<Header
<CourseHeader
courseTitle={courseMetadata.title}
courseNumber={courseMetadata.number}
courseOrg={courseMetadata.org}
data-testid="header"
/>
{!isEnabled && <DemoWarning />}
<CTA />
<NotificationsBanner />
<main data-testid="main">
<main>
<ListView />
</main>
<FooterSlot />
<Footer logo={process.env.LOGO_POWERED_BY_OPEN_EDX_URL_SVG} />
</div>
</Router>
);

View File

@@ -1,7 +1,7 @@
// frontend-app-*/src/index.scss
@import "~@edx/brand/paragon/fonts";
@import "~@edx/brand/paragon/variables";
@import "~@openedx/paragon/scss/core/core";
@import "~@edx/paragon/scss/core/core";
@import "~@edx/brand/paragon/overrides";
$fa-font-path: "~font-awesome/fonts";
@@ -42,6 +42,32 @@ $input-focus-box-shadow: $input-box-shadow; // hack to get upgrade to paragon 4.
}
}
.course-header {
min-width: 0;
border-bottom: 1px solid black;
.course-title-lockup {
min-width: 0;
span {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
padding-bottom: 0.1rem;
}
}
.user-dropdown {
.btn {
height: 3rem;
@media (max-width: -1 + map-get($grid-breakpoints, "sm")) {
padding: 0 0.5rem;
}
}
}
}
#paragon-portal-root {
.pgn__modal-layer {
.pgn__modal-close-container {

View File

@@ -1,5 +1,9 @@
import React from 'react';
import { shallow } from '@edx/react-unit-test-utils';
import { shallow } from 'enzyme';
import Footer from '@edx/frontend-component-footer';
import ListView from 'containers/ListView';
import { App } from './App';
@@ -12,17 +16,15 @@ jest.mock('data/redux', () => ({
},
}));
jest.mock('@edx/frontend-component-header', () => ({
LearningHeader: 'Header',
}));
jest.mock('@edx/frontend-component-footer', () => ({ FooterSlot: 'Footer' }));
jest.mock('@edx/frontend-component-footer', () => 'Footer');
jest.mock('containers/DemoWarning', () => 'DemoWarning');
jest.mock('containers/CTA', () => 'CTA');
jest.mock('containers/ListView', () => 'ListView');
jest.mock('components/Head', () => 'Head');
jest.mock('containers/CourseHeader', () => 'CourseHeader');
const logo = 'fakeLogo.png';
let el;
let router;
describe('App router component', () => {
const props = {
@@ -34,31 +36,26 @@ describe('App router component', () => {
isEnabled: true,
};
test('snapshot: enabled', () => {
expect(shallow(<App {...props} />).snapshot).toMatchSnapshot();
expect(shallow(<App {...props} />)).toMatchSnapshot();
});
test('snapshot: disabled (show demo warning)', () => {
expect(shallow(<App {...props} isEnabled={false} />).snapshot).toMatchSnapshot();
expect(shallow(<App {...props} isEnabled={false} />)).toMatchSnapshot();
});
describe('component', () => {
beforeEach(() => {
process.env.LOGO_POWERED_BY_OPEN_EDX_URL_SVG = logo;
el = shallow(<App {...props} />);
router = el.childAt(0);
});
describe('Router', () => {
test('Routing - ListView is only route', () => {
expect(el.instance.findByTestId('main')[0].children).toHaveLength(1);
expect(el.instance.findByTestId('main')[0].children[0].type).toBe('ListView');
expect(router.find('main')).toEqual(shallow(
<main><ListView /></main>,
));
});
});
test('Header to use courseMetadata props', () => {
const {
courseTitle,
courseNumber,
courseOrg,
} = el.instance.findByTestId('header')[0].props;
expect(courseTitle).toEqual(props.courseMetadata.title);
expect(courseNumber).toEqual(props.courseMetadata.number);
expect(courseOrg).toEqual(props.courseMetadata.org);
test('Footer logo drawn from env variable', () => {
expect(router.find(Footer).props().logo).toEqual(logo);
});
});
});

View File

@@ -3,22 +3,18 @@
exports[`App router component snapshot: disabled (show demo warning) 1`] = `
<BrowserRouter>
<div>
<Head />
<Header
<CourseHeader
courseNumber="course-number"
courseOrg="course-org"
courseTitle="course-title"
data-testid="header"
/>
<DemoWarning />
<CTA />
<NotificationsBanner />
<main
data-testid="main"
>
<main>
<ListView />
</main>
<FooterSlot />
<Footer
logo="https://edx-cdn.org/v3/stage/open-edx-tag.svg"
/>
</div>
</BrowserRouter>
`;
@@ -26,21 +22,17 @@ exports[`App router component snapshot: disabled (show demo warning) 1`] = `
exports[`App router component snapshot: enabled 1`] = `
<BrowserRouter>
<div>
<Head />
<Header
<CourseHeader
courseNumber="course-number"
courseOrg="course-org"
courseTitle="course-title"
data-testid="header"
/>
<CTA />
<NotificationsBanner />
<main
data-testid="main"
>
<main>
<ListView />
</main>
<FooterSlot />
<Footer
logo="https://edx-cdn.org/v3/stage/open-edx-tag.svg"
/>
</div>
</BrowserRouter>
`;

View File

@@ -1,24 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`app registry subscribe: APP_INIT_ERROR. snapshot: displays an ErrorPage to root element 1`] = `
<ErrorPage
message="test-error-message"
/>
`;
exports[`app registry subscribe: APP_READY. links App to root element 1`] = `
<AppProvider
store={
{
"dispatch": [Function],
"getState": [Function],
"replaceReducer": [Function],
"subscribe": [Function],
Symbol(Symbol.observable): [Function],
}
}
wrapWithRouter={false}
>
<App />
</AppProvider>
`;

View File

@@ -1,8 +1,7 @@
import React from 'react';
import PropTypes from 'prop-types';
import { AlertModal, ActionRow, Button } from '@openedx/paragon';
import { nullMethod } from 'hooks';
import { AlertModal, ActionRow, Button } from '@edx/paragon';
export const ConfirmModal = ({
title,
@@ -16,7 +15,7 @@ export const ConfirmModal = ({
<AlertModal
className="confirm-modal"
title={title}
onClose={nullMethod}
onClose={() => ({})}
isOpen={isOpen}
footerNode={(
<ActionRow>

View File

@@ -1,4 +1,4 @@
import { shallow } from '@edx/react-unit-test-utils';
import { shallow } from 'enzyme';
import { ConfirmModal } from './ConfirmModal';
@@ -13,9 +13,9 @@ describe('ConfirmModal', () => {
onConfirm: jest.fn().mockName('this.props.onConfirm'),
};
test('snapshot: closed', () => {
expect(shallow(<ConfirmModal {...props} />).snapshot).toMatchSnapshot();
expect(shallow(<ConfirmModal {...props} />)).toMatchSnapshot();
});
test('snapshot: open', () => {
expect(shallow(<ConfirmModal {...props} isOpen />).snapshot).toMatchSnapshot();
expect(shallow(<ConfirmModal {...props} isOpen />)).toMatchSnapshot();
});
});

View File

@@ -6,7 +6,7 @@ import {
ActionRow,
AlertModal,
Button,
} from '@openedx/paragon';
} from '@edx/paragon';
import messages from './messages';

View File

@@ -1,5 +1,5 @@
import React from 'react';
import { shallow } from '@edx/react-unit-test-utils';
import { shallow } from 'enzyme';
import { formatMessage } from 'testUtils';
import { DemoAlert } from '.';
@@ -11,6 +11,6 @@ describe('DemoAlert component', () => {
isOpen: true,
onClose: jest.fn().mockName('props.onClose'),
};
expect(shallow(<DemoAlert {...props} />).snapshot).toMatchSnapshot();
expect(shallow(<DemoAlert {...props} />)).toMatchSnapshot();
});
});

View File

@@ -1,5 +1,5 @@
import React from 'react';
import { shallow } from '@edx/react-unit-test-utils';
import { shallow } from 'enzyme';
import filesize from 'filesize';
import FilePopoverContent from '.';
@@ -19,19 +19,20 @@ describe('FilePopoverContent', () => {
el = shallow(<FilePopoverContent {...props} />);
});
describe('snapshot', () => {
test('default', () => expect(el.snapshot).toMatchSnapshot());
test('default', () => expect(el).toMatchSnapshot());
test('invalid size', () => {
el = shallow(<FilePopoverContent {...props} size={null} />);
expect(el.snapshot).toMatchSnapshot();
el.setProps({
size: null,
});
expect(el).toMatchSnapshot();
});
});
describe('behavior', () => {
test('content', () => {
const childElements = el.instance.children;
expect(childElements[0].children[2].el).toContain(props.name);
expect(childElements[1].children[2].el).toContain(props.description);
expect(childElements[2].children[2].el).toContain(filesize(props.size));
expect(el.text()).toContain(props.name);
expect(el.text()).toContain(props.description);
expect(el.text()).toContain(filesize(props.size));
});
});
});

View File

@@ -1,8 +1,8 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Alert, Button } from '@openedx/paragon';
import { Info } from '@openedx/paragon/icons';
import { Alert, Button } from '@edx/paragon';
import { Info } from '@edx/paragon/icons';
import { FormattedMessage } from '@edx/frontend-platform/i18n';
const messageShape = PropTypes.shape({

View File

@@ -1,5 +1,5 @@
import React from 'react';
import { shallow } from '@edx/react-unit-test-utils';
import { shallow } from 'enzyme';
import ErrorBanner from './ErrorBanner';
@@ -31,20 +31,16 @@ describe('Error Banner component', () => {
});
test('snapshot', () => {
expect(el.snapshot).toMatchSnapshot();
expect(el).toMatchSnapshot();
});
describe('component', () => {
test('children node', () => {
const childElement = el.instance.children[1];
const child = shallow(children);
expect(childElement.type).toEqual(child.type);
expect(childElement.children[0].el).toEqual(child.children[0].el);
expect(el.containsMatchingElement(children)).toEqual(true);
});
test('verify actions', () => {
const { actions } = el.instance.findByType('Alert')[0].props;
const actions = el.find('Alert').prop('actions');
expect(actions).toHaveLength(props.actions.length);
actions.forEach((action, index) => {
@@ -56,8 +52,8 @@ describe('Error Banner component', () => {
});
test('verify heading', () => {
const heading = el.instance.findByType('FormattedMessage')[0];
expect(heading.props).toEqual(props.headingMessage);
const heading = el.find('FormattedMessage');
expect(heading.props()).toEqual(props.headingMessage);
});
});
});

View File

@@ -1,6 +1,6 @@
import React from 'react';
import { Alert, Spinner } from '@openedx/paragon';
import { Alert, Spinner } from '@edx/paragon';
export const LoadingBanner = () => (
<Alert variant="info">

View File

@@ -1,11 +1,11 @@
import React from 'react';
import { shallow } from '@edx/react-unit-test-utils';
import { shallow } from 'enzyme';
import LoadingBanner from './LoadingBanner';
describe('Loading Banner component', () => {
test('snapshot', () => {
const el = shallow(<LoadingBanner />);
expect(el.snapshot).toMatchSnapshot();
expect(el).toMatchSnapshot();
});
});

View File

@@ -3,7 +3,7 @@
exports[`Error Banner component snapshot 1`] = `
<Alert
actions={
[
Array [
<Button
onClick={[MockFunction action1.onClick]}
variant="outline-primary"

View File

@@ -1,5 +1,5 @@
import React from 'react';
import { shallow } from '@edx/react-unit-test-utils';
import { shallow } from 'enzyme';
import ImageRenderer from './ImageRenderer';
@@ -16,6 +16,6 @@ describe('Image Renderer Component', () => {
el = shallow(<ImageRenderer {...props} />);
});
test('snapshot', () => {
expect(el.snapshot).toMatchSnapshot();
expect(el).toMatchSnapshot();
});
});

View File

@@ -4,82 +4,146 @@ import PropTypes from 'prop-types';
import { pdfjs, Document, Page } from 'react-pdf';
import {
Icon, Form, ActionRow, IconButton,
} from '@openedx/paragon';
import { ChevronLeft, ChevronRight } from '@openedx/paragon/icons';
} from '@edx/paragon';
import { ChevronLeft, ChevronRight } from '@edx/paragon/icons';
import pdfjsWorker from 'react-pdf/dist/esm/pdf.worker.entry';
import 'react-pdf/dist/esm/Page/AnnotationLayer.css';
import { rendererHooks } from './pdfHooks';
pdfjs.GlobalWorkerOptions.workerSrc = pdfjsWorker;
/**
* <PDFRenderer />
*/
export const PDFRenderer = ({
onError,
onSuccess,
url,
}) => {
const {
pageNumber,
numPages,
relativeHeight,
wrapperRef,
onDocumentLoadSuccess,
onLoadPageSuccess,
onDocumentLoadError,
onInputPageChange,
onNextPageButtonClick,
onPrevPageButtonClick,
hasNext,
hasPrev,
} = rendererHooks({ onError, onSuccess });
export class PDFRenderer extends React.Component {
static INITIAL_STATE = {
pageNumber: 1,
numPages: 1,
relativeHeight: 0,
};
return (
<div ref={wrapperRef} className="pdf-renderer">
<Document
file={url}
onLoadSuccess={onDocumentLoadSuccess}
onLoadError={onDocumentLoadError}
>
{/* <Outline /> */}
<div className="page-wrapper" style={{ height: relativeHeight }}>
<Page pageNumber={pageNumber} onLoadSuccess={onLoadPageSuccess} />
</div>
</Document>
<ActionRow className="d-flex justify-content-center m-0">
<IconButton
size="inline"
alt="previous pdf page"
iconAs={Icon}
src={ChevronLeft}
disabled={!hasPrev}
onClick={onPrevPageButtonClick}
/>
<Form.Group className="d-flex align-items-center m-0">
<Form.Label isInline>Page </Form.Label>
<Form.Control
type="number"
min={0}
max={numPages}
value={pageNumber}
onChange={onInputPageChange}
constructor(props) {
super(props);
this.state = { ...PDFRenderer.INITIAL_STATE };
this.wrapperRef = React.createRef();
this.onDocumentLoadSuccess = this.onDocumentLoadSuccess.bind(this);
this.onDocumentLoadError = this.onDocumentLoadError.bind(this);
this.onLoadPageSuccess = this.onLoadPageSuccess.bind(this);
this.onPrevPageButtonClick = this.onPrevPageButtonClick.bind(this);
this.onNextPageButtonClick = this.onNextPageButtonClick.bind(this);
this.onInputPageChange = this.onInputPageChange.bind(this);
}
onDocumentLoadSuccess = ({ numPages }) => {
this.props.onSuccess();
this.setState({ numPages });
};
onLoadPageSuccess = (page) => {
const pageWidth = page.view[2];
const pageHeight = page.view[3];
const wrapperHeight = this.wrapperRef.current.getBoundingClientRect().width;
const relativeHeight = (wrapperHeight * pageHeight) / pageWidth;
if (relativeHeight !== this.state.relativeHeight) {
this.setState({ relativeHeight });
}
};
onDocumentLoadError = (error) => {
let status;
switch (error.name) {
case 'MissingPDFException':
status = 404;
break;
default:
status = 500;
break;
}
this.props.onError(status);
};
onInputPageChange = ({ target: { value } }) => {
this.setPageNumber(parseInt(value, 10));
}
onPrevPageButtonClick = () => {
this.setPageNumber(this.state.pageNumber - 1);
}
onNextPageButtonClick = () => {
this.setPageNumber(this.state.pageNumber + 1);
}
setPageNumber(pageNumber) {
if (pageNumber > 0 && pageNumber <= this.state.numPages) {
this.setState({ pageNumber });
}
}
get hasNext() {
return this.state.pageNumber < this.state.numPages;
}
get hasPrev() {
return this.state.pageNumber > 1;
}
render() {
return (
<div ref={this.wrapperRef} className="pdf-renderer">
<Document
file={this.props.url}
onLoadSuccess={this.onDocumentLoadSuccess}
onLoadError={this.onDocumentLoadError}
>
{/* <Outline /> */}
<div
className="page-wrapper"
style={{
height: this.state.relativeHeight,
}}
>
<Page
pageNumber={this.state.pageNumber}
onLoadSuccess={this.onLoadPageSuccess}
/>
</div>
</Document>
<ActionRow className="d-flex justify-content-center m-0">
<IconButton
size="inline"
alt="previous pdf page"
iconAs={Icon}
src={ChevronLeft}
disabled={!this.hasPrev}
onClick={this.onPrevPageButtonClick}
/>
<Form.Label isInline> of {numPages}</Form.Label>
</Form.Group>
<IconButton
size="inline"
alt="next pdf page"
iconAs={Icon}
src={ChevronRight}
disabled={!hasNext}
onClick={onNextPageButtonClick}
/>
</ActionRow>
</div>
);
};
<Form.Group className="d-flex align-items-center m-0">
<Form.Label isInline>Page </Form.Label>
<Form.Control
type="number"
min={0}
max={this.state.numPages}
value={this.state.pageNumber}
onChange={this.onInputPageChange}
/>
<Form.Label isInline> of {this.state.numPages}</Form.Label>
</Form.Group>
<IconButton
size="inline"
alt="next pdf page"
iconAs={Icon}
src={ChevronRight}
disabled={!this.hasNext}
onClick={this.onNextPageButtonClick}
/>
</ActionRow>
</div>
);
}
}
PDFRenderer.defaultProps = {};

View File

@@ -1,57 +1,221 @@
import React from 'react';
import { shallow } from '@edx/react-unit-test-utils';
import { shallow } from 'enzyme';
import { Document, Page } from 'react-pdf';
import { Form, IconButton } from '@edx/paragon';
import PDFRenderer from './PDFRenderer';
import * as hooks from './pdfHooks';
jest.mock('react-pdf', () => ({
pdfjs: { GlobalWorkerOptions: {} },
Document: () => 'Document',
Page: () => 'Page',
}));
jest.mock('./pdfHooks', () => ({
rendererHooks: jest.fn(),
}));
describe('PDF Renderer Component', () => {
const props = {
url: 'some_url.pdf',
onError: jest.fn().mockName('this.props.onError'),
onSuccess: jest.fn().mockName('this.props.onSuccess'),
};
const hookProps = {
pageNumber: 1,
numPages: 10,
relativeHeight: 200,
wrapperRef: { current: 'hooks.wrapperRef' },
onDocumentLoadSuccess: jest.fn().mockName('hooks.onDocumentLoadSuccess'),
onLoadPageSuccess: jest.fn().mockName('hooks.onLoadPageSuccess'),
onDocumentLoadError: jest.fn().mockName('hooks.onDocumentLoadError'),
onInputPageChange: jest.fn().mockName('hooks.onInputPageChange'),
onNextPageButtonClick: jest.fn().mockName('hooks.onNextPageButtonClick'),
onPrevPageButtonClick: jest.fn().mockName('hooks.onPrevPageButtonClick'),
hasNext: true,
hasPref: false,
};
beforeEach(() => {
jest.clearAllMocks();
});
props.onError = jest.fn().mockName('this.props.onError');
props.onSuccess = jest.fn().mockName('this.props.onSuccess');
let el;
describe('snapshots', () => {
test('first page, prev is disabled', () => {
hooks.rendererHooks.mockReturnValue(hookProps);
expect(shallow(<PDFRenderer {...props} />).snapshot).toMatchSnapshot();
beforeEach(() => {
el = shallow(<PDFRenderer {...props} />);
el.instance().onDocumentLoadSuccess = jest
.fn()
.mockName('onDocumentLoadSuccess');
el.instance().onDocumentLoadError = jest
.fn()
.mockName('onDocumentLoadError');
el.instance().onLoadPageSuccess = jest.fn().mockName('onLoadPageSuccess');
});
test('on last page, next is disabled', () => {
hooks.rendererHooks.mockReturnValue({
...hookProps,
pageNumber: hookProps.numPages,
hasNext: false,
hasPrev: true,
test('snapshot', () => {
el.instance().onPrevPageButtonClick = jest
.fn()
.mockName('onPrevPageButtonClick');
el.instance().onNextPageButtonClick = jest
.fn()
.mockName('onNextPageButtonClick');
el.instance().onInputPageChange = jest.fn().mockName('onInputPageChange');
expect(el.instance().render()).toMatchSnapshot();
});
});
describe('Component', () => {
const numPages = 99;
const pageNumber = 234;
beforeEach(() => {
el = shallow(<PDFRenderer {...props} />);
});
describe('render', () => {
describe('Top-level document', () => {
let documentEl;
beforeEach(() => { documentEl = el.find(Document); });
it('displays file from props.url', () => {
expect(documentEl.props().file).toEqual(props.url);
});
it('calls this.onDocumentLoadSuccess onLoadSuccess', () => {
expect(documentEl.props().onLoadSuccess).toEqual(el.instance().onDocumentLoadSuccess);
});
it('calls this.onDocumentLoadError onLoadError', () => {
expect(documentEl.props().onLoadError).toEqual(el.instance().onDocumentLoadError);
});
});
describe('Page', () => {
let pageProps;
beforeEach(() => {
el.instance().setState({ pageNumber });
pageProps = el.find(Page).props();
});
it('loads pageNumber from state', () => {
expect(pageProps.pageNumber).toEqual(pageNumber);
});
it('calls onLoadPageSuccess onLoadSuccess', () => {
expect(pageProps.onLoadSuccess).toEqual(el.instance().onLoadPageSuccess);
});
});
describe('pagination ActionRow', () => {
describe('Previous page button', () => {
let hasPrev;
beforeEach(() => {
hasPrev = jest.spyOn(el.instance(), 'hasPrev', 'get').mockReturnValue(false);
});
const btn = () => shallow(el.instance().render()).find(IconButton).at(0).props();
test('disabled iff not this.hasPrev', () => {
expect(btn().disabled).toEqual(true);
hasPrev.mockReturnValue(true);
expect(btn().disabled).toEqual(false);
});
it('calls onPrevPageButtonClick onClick', () => {
expect(btn().onClick).toEqual(el.instance().onPrevPageButtonClick);
});
});
describe('page indicator', () => {
const control = () => el.find(Form.Control).at(0).props();
const labels = () => {
const flat = el.find({ isInline: true });
return [0, 1].map(i => flat.at(i).text());
};
beforeEach(() => { el.instance().setState({ numPages, pageNumber }); });
test('labels: Page <state.pageNumber> of <state.numPages>', () => {
expect(`${labels()[0]}${control().value}${labels()[1]}`).toEqual(
`Page ${pageNumber} of ${numPages}`,
);
});
it('loads max from state.numPages', () => expect(control().max).toEqual(numPages));
it('loads value from state.pageNumber', () => {
expect(control().value).toEqual(pageNumber);
});
it('calls onInputPageChange onChange', () => {
expect(control().onChange).toEqual(el.instance().onInputPageChange);
});
});
describe('Next page button', () => {
let hasNext;
beforeEach(() => {
hasNext = jest.spyOn(el.instance(), 'hasNext', 'get').mockReturnValue(false);
});
const btn = () => shallow(el.instance().render()).find(IconButton).at(1).props();
test('disabled iff not this.hasNext', () => {
expect(btn().disabled).toEqual(true);
hasNext.mockReturnValue(true);
expect(btn().disabled).toEqual(false);
});
it('calls onNextPageButtonClick onClick', () => {
expect(btn().onClick).toEqual(el.instance().onNextPageButtonClick);
});
});
});
});
describe('behavior', () => {
test('initial state', () => {
expect(el.instance().state).toEqual(PDFRenderer.INITIAL_STATE);
});
describe('onDocumentLoadSuccess', () => {
test('loads numPages into state', () => {
el.instance().onDocumentLoadSuccess({ numPages });
expect(el.instance().state.numPages).toEqual(numPages);
});
});
describe('onLoadPageSuccess', () => {
const [pageHeight, pageWidth] = [23, 34];
const page = { view: [1, 2, pageWidth, pageHeight] };
const wrapperWidth = 20;
const expected = (wrapperWidth * pageHeight) / pageWidth;
beforeEach(() => {
el.instance().wrapperRef = {
current: {
getBoundingClientRect: () => ({ width: wrapperWidth }),
},
};
});
it('sets relative height if it has changes', () => {
el.instance().onLoadPageSuccess(page);
expect(el.instance().state.relativeHeight).toEqual(expected);
});
it('does not try to set height if has not changes', () => {
el.instance().setState({ relativeHeight: expected });
el.instance().setState = jest.fn();
el.instance().onLoadPageSuccess(page);
expect(el.instance().setState).not.toHaveBeenCalled();
});
});
describe('setPageNumber inheritors', () => {
beforeEach(() => {
el.instance().setPageNumber = jest.fn();
el.instance().setState({ pageNumber });
});
describe('onInputChange', () => {
it('calls setPageNumber with int value of event target value', () => {
el.instance().onInputPageChange({ target: { value: '23' } });
expect(el.instance().setPageNumber).toHaveBeenCalledWith(23);
});
});
describe('onPrevPageButtonClick', () => {
it('calls setPageNumber with state.pageNumber - 1', () => {
el.instance().onPrevPageButtonClick();
expect(el.instance().setPageNumber).toHaveBeenCalledWith(pageNumber - 1);
});
});
describe('onNextPageButtonClick', () => {
it('calls setPageNumber with state.pageNumber + 1', () => {
el.instance().onNextPageButtonClick();
expect(el.instance().setPageNumber).toHaveBeenCalledWith(pageNumber + 1);
});
});
});
describe('setPageNumber', () => {
it('calls setState with pageNumber iff valid', () => {
el.instance().setState({ numPages });
const setState = jest.spyOn(el.instance(), 'setState');
el.instance().setPageNumber(0);
expect(setState).not.toHaveBeenCalled();
el.instance().setPageNumber(numPages + 1);
expect(setState).not.toHaveBeenCalled();
el.instance().setPageNumber(2);
expect(setState).toHaveBeenCalledWith({ pageNumber: 2 });
});
});
describe('hasNext getter', () => {
it('returns true iff state.pageNumber < state.numPages', () => {
el.instance().setState({ pageNumber: 1, numPages: 1 });
expect(el.instance().hasNext).toEqual(false);
el.instance().setState({ pageNumber: 1, numPages: 2 });
expect(el.instance().hasNext).toEqual(true);
});
});
describe('hasPrev getter', () => {
it('returns true iff state.pageNumber > 1', () => {
el.instance().setState({ pageNumber: 1 });
expect(el.instance().hasPrev).toEqual(false);
el.instance().setState({ pageNumber: 2 });
expect(el.instance().hasPrev).toEqual(true);
});
});
expect(shallow(<PDFRenderer {...props} />).snapshot).toMatchSnapshot();
});
});
});

View File

@@ -1,9 +1,18 @@
import React from 'react';
import React, { useMemo, useState } from 'react';
import PropTypes from 'prop-types';
import { rendererHooks } from './textHooks';
import { get } from 'axios';
const TXTRenderer = ({ url, onError, onSuccess }) => {
const { content } = rendererHooks({ url, onError, onSuccess });
const [content, setContent] = useState('');
useMemo(() => {
get(url)
.then(({ data }) => {
onSuccess();
setContent(data);
})
.catch(({ response }) => onError(response.status));
}, [url]);
return (
<pre className="txt-renderer">
{content}

View File

@@ -1,23 +1,25 @@
import React from 'react';
import { shallow } from '@edx/react-unit-test-utils';
import { shallow } from 'enzyme';
import TXTRenderer from './TXTRenderer';
jest.mock('./textHooks', () => {
const content = 'test-content';
return {
content,
rendererHooks: (args) => ({ content, rendererHooks: args }),
};
});
jest.mock('axios', () => ({
get: jest.fn((...args) => Promise.resolve({ data: `Content of ${args}` })),
}));
describe('TXT Renderer Component', () => {
const props = {
url: 'some_url.txt',
onError: jest.fn().mockName('this.props.onError'),
onSuccess: jest.fn().mockName('this.props.onSuccess'),
};
props.onError = jest.fn().mockName('this.props.onError');
props.onSuccess = jest.fn().mockName('this.props.onSuccess');
let el;
beforeEach(() => {
el = shallow(<TXTRenderer {...props} />);
});
test('snapshot', () => {
expect(shallow(<TXTRenderer {...props} />).snapshot).toMatchSnapshot();
expect(el).toMatchSnapshot();
});
});

View File

@@ -1,24 +1,24 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`PDF Renderer Component snapshots first page, prev is disabled 1`] = `
exports[`PDF Renderer Component snapshots snapshot 1`] = `
<div
className="pdf-renderer"
>
<Document
file="some_url.pdf"
onLoadError={[MockFunction hooks.onDocumentLoadError]}
onLoadSuccess={[MockFunction hooks.onDocumentLoadSuccess]}
onLoadError={[MockFunction onDocumentLoadError]}
onLoadSuccess={[MockFunction onDocumentLoadSuccess]}
>
<div
className="page-wrapper"
style={
{
"height": 200,
Object {
"height": 0,
}
}
>
<Page
onLoadSuccess={[MockFunction hooks.onLoadPageSuccess]}
onLoadSuccess={[MockFunction onLoadPageSuccess]}
pageNumber={1}
/>
</div>
@@ -30,7 +30,7 @@ exports[`PDF Renderer Component snapshots first page, prev is disabled 1`] = `
alt="previous pdf page"
disabled={true}
iconAs="Icon"
onClick={[MockFunction hooks.onPrevPageButtonClick]}
onClick={[MockFunction onPrevPageButtonClick]}
size="inline"
src={[MockFunction icons.ChevronLeft]}
/>
@@ -43,9 +43,9 @@ exports[`PDF Renderer Component snapshots first page, prev is disabled 1`] = `
Page
</Form.Label>
<Form.Control
max={10}
max={1}
min={0}
onChange={[MockFunction hooks.onInputPageChange]}
onChange={[MockFunction onInputPageChange]}
type="number"
value={1}
/>
@@ -53,82 +53,14 @@ exports[`PDF Renderer Component snapshots first page, prev is disabled 1`] = `
isInline={true}
>
of
10
</Form.Label>
</Form.Group>
<IconButton
alt="next pdf page"
disabled={false}
iconAs="Icon"
onClick={[MockFunction hooks.onNextPageButtonClick]}
size="inline"
src={[MockFunction icons.ChevronRight]}
/>
</ActionRow>
</div>
`;
exports[`PDF Renderer Component snapshots on last page, next is disabled 1`] = `
<div
className="pdf-renderer"
>
<Document
file="some_url.pdf"
onLoadError={[MockFunction hooks.onDocumentLoadError]}
onLoadSuccess={[MockFunction hooks.onDocumentLoadSuccess]}
>
<div
className="page-wrapper"
style={
{
"height": 200,
}
}
>
<Page
onLoadSuccess={[MockFunction hooks.onLoadPageSuccess]}
pageNumber={10}
/>
</div>
</Document>
<ActionRow
className="d-flex justify-content-center m-0"
>
<IconButton
alt="previous pdf page"
disabled={false}
iconAs="Icon"
onClick={[MockFunction hooks.onPrevPageButtonClick]}
size="inline"
src={[MockFunction icons.ChevronLeft]}
/>
<Form.Group
className="d-flex align-items-center m-0"
>
<Form.Label
isInline={true}
>
Page
</Form.Label>
<Form.Control
max={10}
min={0}
onChange={[MockFunction hooks.onInputPageChange]}
type="number"
value={10}
/>
<Form.Label
isInline={true}
>
of
10
1
</Form.Label>
</Form.Group>
<IconButton
alt="next pdf page"
disabled={true}
iconAs="Icon"
onClick={[MockFunction hooks.onNextPageButtonClick]}
onClick={[MockFunction onNextPageButtonClick]}
size="inline"
src={[MockFunction icons.ChevronRight]}
/>

View File

@@ -4,6 +4,6 @@ exports[`TXT Renderer Component snapshot 1`] = `
<pre
className="txt-renderer"
>
test-content
Content of some_url.txt
</pre>
`;

View File

@@ -1,81 +0,0 @@
import { useState, useRef } from 'react';
import { pdfjs } from 'react-pdf';
import pdfjsWorker from 'react-pdf/dist/esm/pdf.worker.entry';
import 'react-pdf/dist/esm/Page/AnnotationLayer.css';
import { ErrorStatuses } from 'data/constants/requests';
import { StrictDict } from 'utils';
import * as module from './pdfHooks';
pdfjs.GlobalWorkerOptions.workerSrc = pdfjsWorker;
export const errors = StrictDict({
missingPDF: 'MissingPDFException',
});
export const state = StrictDict({
pageNumber: (val) => useState(val),
numPages: (val) => useState(val),
relativeHeight: (val) => useState(val),
});
export const initialState = {
pageNumber: 1,
numPages: 1,
relativeHeight: 1,
};
export const safeSetPageNumber = ({ numPages, rawSetPageNumber }) => (pageNumber) => {
if (pageNumber > 0 && pageNumber <= numPages) {
rawSetPageNumber(pageNumber);
}
};
export const rendererHooks = ({
onError,
onSuccess,
}) => {
const [pageNumber, rawSetPageNumber] = module.state.pageNumber(initialState.pageNumber);
const [numPages, setNumPages] = module.state.numPages(initialState.numPages);
const [relativeHeight, setRelativeHeight] = module.state.relativeHeight(
initialState.relativeHeight,
);
const setPageNumber = module.safeSetPageNumber({ numPages, rawSetPageNumber });
const wrapperRef = useRef();
return {
pageNumber,
numPages,
relativeHeight,
wrapperRef,
onDocumentLoadSuccess: (args) => {
onSuccess();
setNumPages(args.numPages);
},
onLoadPageSuccess: (page) => {
const pageWidth = page.view[2];
const pageHeight = page.view[3];
const wrapperHeight = wrapperRef.current.getBoundingClientRect().width;
const newHeight = (wrapperHeight * pageHeight) / pageWidth;
setRelativeHeight(newHeight);
},
onDocumentLoadError: (error) => {
let status;
if (error.name === errors.missingPDF) {
status = ErrorStatuses.notFound;
} else {
status = ErrorStatuses.serverError;
}
onError(status);
},
onInputPageChange: ({ target: { value } }) => setPageNumber(parseInt(value, 10)),
onPrevPageButtonClick: () => setPageNumber(pageNumber - 1),
onNextPageButtonClick: () => setPageNumber(pageNumber + 1),
hasNext: pageNumber < numPages,
hasPrev: pageNumber > 1,
};
};

View File

@@ -1,148 +0,0 @@
import React from 'react';
import { MockUseState } from 'testUtils';
import { keyStore } from 'utils';
import { ErrorStatuses } from 'data/constants/requests';
import * as hooks from './pdfHooks';
jest.mock('react-pdf', () => ({
pdfjs: { GlobalWorkerOptions: {} },
Document: () => 'Document',
Page: () => 'Page',
}));
const state = new MockUseState(hooks);
const hookKeys = keyStore(hooks);
const testValue = 'my-test-value';
describe('PDF Renderer hooks', () => {
beforeAll(() => {
jest.clearAllMocks();
});
describe('state hooks', () => {
state.testGetter(state.keys.pageNumber);
state.testGetter(state.keys.numPages);
state.testGetter(state.keys.relativeHeight);
});
describe('non-state hooks', () => {
beforeEach(() => state.mock());
afterEach(() => state.restore());
describe('safeSetPageNumber', () => {
it('returns value handler that sets page number if valid', () => {
const rawSetPageNumber = jest.fn();
const numPages = 10;
hooks.safeSetPageNumber({ numPages, rawSetPageNumber })(0);
expect(rawSetPageNumber).not.toHaveBeenCalled();
hooks.safeSetPageNumber({ numPages, rawSetPageNumber })(numPages + 1);
expect(rawSetPageNumber).not.toHaveBeenCalled();
hooks.safeSetPageNumber({ numPages, rawSetPageNumber })(numPages - 1);
expect(rawSetPageNumber).toHaveBeenCalledWith(numPages - 1);
});
});
describe('rendererHooks', () => {
const props = {
url: 'some_url.pdf',
onError: jest.fn().mockName('this.props.onError'),
onSuccess: jest.fn().mockName('this.props.onSuccess'),
};
let setPageNumber;
let hook;
let mockSetPageNumber;
let mockSafeSetPageNumber;
beforeEach(() => {
mockSetPageNumber = jest.fn(val => ({ setPageNumber: { val } }));
mockSafeSetPageNumber = jest.fn(() => mockSetPageNumber);
setPageNumber = jest.spyOn(hooks, hookKeys.safeSetPageNumber)
.mockImplementation(mockSafeSetPageNumber);
hook = hooks.rendererHooks(props);
});
afterAll(() => {
setPageNumber.mockRestore();
});
describe('returned object', () => {
Object.keys(state.keys).forEach(key => {
test(`${key} tied to store and initialized from initialState`, () => {
expect(hook[key]).toEqual(hooks.initialState[key]);
expect(hook[key]).toEqual(state.stateVals[key]);
});
});
});
test('wrapperRef passed as react ref', () => {
expect(hook.wrapperRef.useRef).toEqual(true);
});
describe('onDocumentLoadSuccess', () => {
it('calls onSuccess and sets numPages based on args', () => {
hook.onDocumentLoadSuccess({ numPages: testValue });
expect(props.onSuccess).toHaveBeenCalled();
expect(state.setState.numPages).toHaveBeenCalledWith(testValue);
});
});
describe('onLoadPageSuccess', () => {
it('sets relative height based on page size', () => {
const width = 23;
React.useRef.mockReturnValueOnce({
current: {
getBoundingClientRect: () => ({ width }),
},
});
const [pageWidth, pageHeight] = [20, 30];
const page = { view: [0, 0, pageWidth, pageHeight] };
hook = hooks.rendererHooks(props);
const height = (width * pageHeight) / pageWidth;
hook.onLoadPageSuccess(page);
expect(state.setState.relativeHeight).toHaveBeenCalledWith(height);
});
});
describe('onDocumentLoadError', () => {
it('calls onError with notFound error if error is missingPDF error', () => {
hook.onDocumentLoadError({ name: hooks.errors.missingPDF });
expect(props.onError).toHaveBeenCalledWith(ErrorStatuses.notFound);
});
it('calls onError with serverError by default', () => {
hook.onDocumentLoadError({ name: testValue });
expect(props.onError).toHaveBeenCalledWith(ErrorStatuses.serverError);
});
});
describe('onInputPageChange', () => {
it('calls setPageNumber with int event target value', () => {
hook.onInputPageChange({ target: { value: '2.3' } });
expect(mockSetPageNumber).toHaveBeenCalledWith(2);
});
});
describe('onPrevPageButtonClick', () => {
it('calls setPageNumber with current page number - 1', () => {
hook.onPrevPageButtonClick();
expect(mockSetPageNumber).toHaveBeenCalledWith(hook.pageNumber - 1);
});
});
describe('onNextPageButtonClick', () => {
it('calls setPageNumber with current page number + 1', () => {
hook.onNextPageButtonClick();
expect(mockSetPageNumber).toHaveBeenCalledWith(hook.pageNumber + 1);
});
});
test('hasNext returns true iff pageNumber is less than total number of pages', () => {
state.mockVal(state.keys.numPages, 10);
state.mockVal(state.keys.pageNumber, 9);
hook = hooks.rendererHooks(props);
expect(hook.hasNext).toEqual(true);
state.mockVal(state.keys.pageNumber, 10);
hook = hooks.rendererHooks(props);
expect(hook.hasNext).toEqual(false);
});
test('hasPrev returns true iff pageNumber is greater than 1', () => {
state.mockVal(state.keys.pageNumber, 1);
hook = hooks.rendererHooks(props);
expect(hook.hasPrev).toEqual(false);
state.mockVal(state.keys.pageNumber, 0);
hook = hooks.rendererHooks(props);
expect(hook.hasPrev).toEqual(false);
state.mockVal(state.keys.pageNumber, 2);
hook = hooks.rendererHooks(props);
expect(hook.hasPrev).toEqual(true);
});
});
});
});

View File

@@ -1,34 +0,0 @@
import { useEffect, useState } from 'react';
import { get } from 'axios';
import { StrictDict } from 'utils';
import * as module from './textHooks';
export const state = StrictDict({
content: (val) => useState(val),
});
export const fetchFile = async ({
setContent,
url,
onError,
onSuccess,
}) => get(url)
.then(({ data }) => {
onSuccess();
setContent(data);
})
.catch((e) => onError(e.response.status));
export const rendererHooks = ({ url, onError, onSuccess }) => {
const [content, setContent] = module.state.content('');
useEffect(() => {
module.fetchFile({
setContent,
url,
onError,
onSuccess,
});
}, [onError, onSuccess, setContent, url]);
return { content };
};

View File

@@ -1,94 +0,0 @@
/* eslint-disable prefer-promise-reject-errors */
import { useEffect } from 'react';
import * as axios from 'axios';
import { keyStore } from 'utils';
import { MockUseState } from 'testUtils';
import * as hooks from './textHooks';
jest.mock('axios', () => ({
get: jest.fn(),
}));
const hookKeys = keyStore(hooks);
const state = new MockUseState(hooks);
let hook;
const testValue = 'test-value';
const props = {
url: 'test-url',
onError: jest.fn(),
onSuccess: jest.fn(),
};
describe('Text file preview hooks', () => {
beforeEach(() => {
jest.clearAllMocks();
});
describe('state hooks', () => {
state.testGetter(state.keys.content);
});
describe('non-state hooks', () => {
beforeEach(() => {
state.mock();
});
afterEach(() => {
state.restore();
});
describe('rendererHooks', () => {
it('returns content tied to hook state', () => {
hook = hooks.rendererHooks(props);
expect(hook.content).toEqual(state.stateVals.content);
expect(hook.content).toEqual('');
});
describe('initialization behavior', () => {
let cb;
let prereqs;
const loadHook = () => {
hook = hooks.rendererHooks(props);
[[cb, prereqs]] = useEffect.mock.calls;
};
it('calls fetchFile method, predicated on setContent, url, and callbacks', () => {
jest.spyOn(hooks, hookKeys.fetchFile).mockImplementationOnce(() => {});
loadHook();
expect(useEffect).toHaveBeenCalled();
expect(prereqs).toEqual([
props.onError,
props.onSuccess,
state.setState.content,
props.url,
]);
expect(hooks.fetchFile).not.toHaveBeenCalled();
cb();
expect(hooks.fetchFile).toHaveBeenCalledWith({
onError: props.onError,
onSuccess: props.onSuccess,
setContent: state.setState.content,
url: props.url,
});
});
});
});
describe('fetchFile', () => {
describe('onSuccess', () => {
it('calls get', async () => {
const testData = 'test-data';
axios.get.mockReturnValueOnce(Promise.resolve({ data: testData }));
await hooks.fetchFile({ ...props, setContent: state.setState.content });
expect(props.onSuccess).toHaveBeenCalled();
expect(state.setState[state.keys.content]).toHaveBeenCalledWith(testData);
});
});
describe('onError', () => {
it('calls get on the passed url when it changes', async () => {
axios.get.mockReturnValueOnce(Promise.reject(
{ response: { status: testValue } },
));
await hooks.fetchFile({ ...props, setContent: state.setState.content });
expect(props.onError).toHaveBeenCalledWith(testValue);
});
});
});
});
});

View File

@@ -1,7 +1,7 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Card, Collapsible } from '@openedx/paragon';
import { Card, Collapsible } from '@edx/paragon';
import FilePopoverContent from 'components/FilePopoverContent';
import FileInfo from './FileInfo';
@@ -12,12 +12,8 @@ import './FileCard.scss';
*/
export const FileCard = ({ file, children }) => (
<Card className="file-card" key={file.name}>
<Collapsible
className="file-collapsible"
defaultOpen
title={<h3 className="file-card-title">{file.name}</h3>}
>
<div className="preview-panel" data-testid="preview-panel">
<Collapsible className="file-collapsible" defaultOpen title={<h3 className="file-card-title">{file.name}</h3>}>
<div className="preview-panel">
<FileInfo><FilePopoverContent {...file} /></FileInfo>
{children}
</div>

View File

@@ -1,4 +1,4 @@
@import "@openedx/paragon/scss/core/core";
@import "@edx/paragon/scss/core/core";
.file-card {
margin: map-get($spacers, 1) 0;
@@ -28,6 +28,6 @@
@include media-breakpoint-down(sm) {
.file-card-title {
width: calc(map-get($container-max-widths, "sm")/2);
width: map-get($container-max-widths, "sm")/2;
}
}

View File

@@ -1,7 +1,7 @@
import React from 'react';
import { shallow } from '@edx/react-unit-test-utils';
import { shallow } from 'enzyme';
import { Collapsible } from '@openedx/paragon';
import { Collapsible } from '@edx/paragon';
import FilePopoverContent from 'components/FilePopoverContent';
import FileInfo from './FileInfo';
@@ -24,19 +24,19 @@ describe('File Preview Card component', () => {
el = shallow(<FileCard {...props}>{children}</FileCard>);
});
test('snapshot', () => {
expect(el.snapshot).toMatchSnapshot();
expect(el).toMatchSnapshot();
});
describe('Component', () => {
test('collapsible title is name header', () => {
const { title } = el.instance.findByType(Collapsible)[0].props;
const title = el.find(Collapsible).prop('title');
expect(title).toEqual(<h3 className="file-card-title">{props.file.name}</h3>);
});
test('forwards children into preview-panel', () => {
const previewPanelChildren = el.instance.findByTestId('preview-panel')[0].children;
expect(previewPanelChildren[0].matches(
const previewPanelChildren = el.find('.preview-panel').children();
expect(previewPanelChildren.at(0).equals(
<FileInfo><FilePopoverContent file={props.file} /></FileInfo>,
));
expect(previewPanelChildren[1].matches(shallow(children))).toEqual(true);
expect(previewPanelChildren.at(1).equals(children)).toEqual(true);
});
});
});

View File

@@ -5,10 +5,9 @@ import {
Button,
OverlayTrigger,
Popover,
} from '@openedx/paragon';
import { InfoOutline } from '@openedx/paragon/icons';
} from '@edx/paragon';
import { InfoOutline } from '@edx/paragon/icons';
import { FormattedMessage } from '@edx/frontend-platform/i18n';
import { nullMethod } from 'hooks';
import messages from './messages';
/**
@@ -20,13 +19,13 @@ export const FileInfo = ({ onClick, children }) => (
placement="right-end"
flip
overlay={(
<Popover id="file-popover" className="overlay-help-popover">
<Popover className="overlay-help-popover">
<Popover.Content>{children}</Popover.Content>
</Popover>
)}
>
<Button
size="sm"
size="small"
variant="tertiary"
onClick={onClick}
iconAfter={InfoOutline}
@@ -37,7 +36,7 @@ export const FileInfo = ({ onClick, children }) => (
);
FileInfo.defaultProps = {
onClick: nullMethod,
onClick: () => {},
};
FileInfo.propTypes = {
onClick: PropTypes.func,

View File

@@ -1,7 +1,7 @@
import React from 'react';
import { shallow } from '@edx/react-unit-test-utils';
import { shallow } from 'enzyme';
import { Popover } from '@openedx/paragon';
import { Popover } from '@edx/paragon';
import FileInfo from './FileInfo';
@@ -13,11 +13,11 @@ describe('File Preview Card component', () => {
el = shallow(<FileInfo {...props}>{children}</FileInfo>);
});
test('snapshot', () => {
expect(el.snapshot).toMatchSnapshot();
expect(el).toMatchSnapshot();
});
describe('Component', () => {
test('overlay with passed children', () => {
const { overlay } = el.instance.props;
const { overlay } = el.at(0).props();
expect(overlay.type).toEqual(Popover);
expect(overlay.props.children).toEqual(<Popover.Content>{children}</Popover.Content>);
});

View File

@@ -1,37 +1,123 @@
import React from 'react';
import PropTypes from 'prop-types';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { StrictDict } from 'utils';
import { FileTypes } from 'data/constants/files';
import { FormattedMessage } from '@edx/frontend-platform/i18n';
import {
PDFRenderer,
ImageRenderer,
TXTRenderer,
} from 'components/FilePreview/BaseRenderers';
import FileCard from './FileCard';
import { ErrorBanner, LoadingBanner } from './Banners';
import { renderHooks } from './hooks';
import messages from './messages';
export const RENDERERS = StrictDict({
[FileTypes.pdf]: PDFRenderer,
[FileTypes.jpg]: ImageRenderer,
[FileTypes.jpeg]: ImageRenderer,
[FileTypes.bmp]: ImageRenderer,
[FileTypes.png]: ImageRenderer,
[FileTypes.txt]: TXTRenderer,
[FileTypes.gif]: ImageRenderer,
[FileTypes.jfif]: ImageRenderer,
[FileTypes.pjpeg]: ImageRenderer,
[FileTypes.pjp]: ImageRenderer,
[FileTypes.svg]: ImageRenderer,
});
export const ERROR_STATUSES = {
404: {
headingMessage: messages.fileNotFoundError,
children: <FormattedMessage {...messages.fileNotFoundError} />,
},
500: {
headingMessage: messages.unknownError,
children: <FormattedMessage {...messages.unknownError} />,
},
};
export const SUPPORTED_TYPES = Object.keys(RENDERERS);
export const getFileType = (fileName) => fileName.split('.').pop()?.toLowerCase();
export const isSupported = (file) => SUPPORTED_TYPES.includes(getFileType(file.name));
/**
* <FileRenderer />
*/
export const FileRenderer = ({
file,
intl,
}) => {
const {
Renderer,
isLoading,
errorStatus,
error,
rendererProps,
} = renderHooks({ file, intl });
return (
<FileCard key={file.downloadUrl} file={file}>
{isLoading && <LoadingBanner />}
{errorStatus ? (
<ErrorBanner {...error} />
) : (
<Renderer {...rendererProps} />
)}
</FileCard>
);
};
export class FileRenderer extends React.Component {
constructor(props) {
super(props);
this.state = {
errorStatus: null,
isLoading: true,
};
this.onError = this.onError.bind(this);
this.onSuccess = this.onSuccess.bind(this);
this.resetState = this.resetState.bind(this);
}
onError(status) {
this.setState({
errorStatus: status,
isLoading: false,
});
}
onSuccess() {
this.setState({
errorStatus: null,
isLoading: false,
});
}
get error() {
const status = this.state.errorStatus;
return {
...ERROR_STATUSES[status] || ERROR_STATUSES[500],
actions: [
{
id: 'retry',
onClick: this.resetState,
message: messages.retryButton,
},
],
};
}
resetState = () => {
this.setState({
errorStatus: null,
isLoading: true,
});
};
render() {
const { file } = this.props;
const Renderer = RENDERERS[getFileType(file.name)];
return (
<FileCard key={file.downloadUrl} file={file}>
{this.state.isLoading && <LoadingBanner />}
{this.state.errorStatus ? (
<ErrorBanner {...this.error} />
) : (
<Renderer
fileName={file.name}
url={file.downloadUrl}
onError={this.onError}
onSuccess={this.onSuccess}
/>
)}
</FileCard>
);
}
}
FileRenderer.defaultProps = {};
FileRenderer.propTypes = {
@@ -39,8 +125,6 @@ FileRenderer.propTypes = {
name: PropTypes.string,
downloadUrl: PropTypes.string,
}).isRequired,
// injected
intl: intlShape.isRequired,
};
export default injectIntl(FileRenderer);
export default FileRenderer;

View File

@@ -1,52 +1,132 @@
import React from 'react';
import { shallow } from '@edx/react-unit-test-utils';
import { shallow } from 'enzyme';
import { formatMessage } from 'testUtils';
import { keyStore } from 'utils';
import { ErrorStatuses } from 'data/constants/requests';
import { FileRenderer } from './FileRenderer';
import * as hooks from './hooks';
import { FileTypes } from 'data/constants/files';
import {
ImageRenderer,
PDFRenderer,
TXTRenderer,
} from 'components/FilePreview/BaseRenderers';
import {
FileRenderer,
getFileType,
ERROR_STATUSES,
RENDERERS,
} from './FileRenderer';
jest.mock('./FileCard', () => 'FileCard');
jest.mock('components/FilePreview/BaseRenderers', () => ({
PDFRenderer: () => 'PDFRenderer',
ImageRenderer: () => 'ImageRenderer',
TXTRenderer: () => 'TXTRenderer',
}));
jest.mock('./Banners', () => ({
ErrorBanner: () => 'ErrorBanner',
LoadingBanner: () => 'LoadingBanner',
}));
const hookKeys = keyStore(hooks);
const props = {
file: {
downloadUrl: 'file download url',
name: 'filename.txt',
},
intl: { formatMessage },
};
describe('FileRenderer', () => {
describe('component', () => {
const supportedTypes = Object.keys(RENDERERS);
const files = [
...supportedTypes.map((fileType, index) => ({
name: `fake_file_${index}.${fileType}`,
description: `file description ${index}`,
downloadUrl: `/url-path/fake_file_${index}.${fileType}`,
})),
];
const els = files.map((file) => {
const el = shallow(<FileRenderer file={file} />);
el.instance().onError = jest.fn().mockName('this.props.onError');
el.instance().onSuccess = jest.fn().mockName('this.props.onSuccess');
return el;
});
describe('snapshot', () => {
test('isLoading, no Error', () => {
const hookProps = {
Renderer: () => 'Renderer',
isloading: true,
errorStatus: null,
error: null,
rendererProps: { prop: 'hooks.rendererProps' },
};
jest.spyOn(hooks, hookKeys.renderHooks).mockReturnValueOnce(hookProps);
expect(shallow(<FileRenderer {...props} />).snapshot).toMatchSnapshot();
els.forEach((el) => {
const file = el.prop('file');
const fileType = getFileType(file.name);
test(`successful rendering ${fileType}`, () => {
el.setState({ isLoading: false });
expect(el.instance().render()).toMatchSnapshot();
});
});
test('is not loading, with error', () => {
const hookProps = {
Renderer: () => 'Renderer',
isloading: false,
errorStatus: ErrorStatuses.serverError,
error: { prop: 'hooks.errorProps' },
rendererProps: { prop: 'hooks.rendererProps' },
Object.keys(ERROR_STATUSES).forEach((status) => {
test(`has error ${status}`, () => {
const el = shallow(<FileRenderer file={files[0]} />);
el.instance().setState({
errorStatus: status,
isLoading: false,
});
el.instance().resetState = jest.fn().mockName('this.resetState');
expect(el.instance().render()).toMatchSnapshot();
});
});
});
describe('component', () => {
describe('uses the correct renderers', () => {
const checkFile = (index, expectedRenderer) => {
const file = files[index];
const el = shallow(<FileRenderer file={file} />);
const renderer = el.find(expectedRenderer);
const { url, fileName } = renderer.props();
expect(renderer).toBeDefined();
expect(url).toEqual(file.downloadUrl);
expect(fileName).toEqual(file.name);
};
jest.spyOn(hooks, hookKeys.renderHooks).mockReturnValueOnce(hookProps);
expect(shallow(<FileRenderer {...props} />).snapshot).toMatchSnapshot();
/**
* The manual process for this is prefer. I want to be more explicit
* of which file correspond to which renderer. If I use RENDERERS dicts,
* this wouldn't be a test.
*/
test(FileTypes.pdf, () => checkFile(0, PDFRenderer));
test(FileTypes.jpg, () => checkFile(1, ImageRenderer));
test(FileTypes.jpeg, () => checkFile(2, ImageRenderer));
test(FileTypes.bmp, () => checkFile(3, ImageRenderer));
test(FileTypes.png, () => checkFile(4, ImageRenderer));
test(FileTypes.txt, () => checkFile(5, TXTRenderer));
test(FileTypes.gif, () => checkFile(6, ImageRenderer));
test(FileTypes.jfif, () => checkFile(7, ImageRenderer));
test(FileTypes.pjpeg, () => checkFile(8, ImageRenderer));
test(FileTypes.pjp, () => checkFile(9, ImageRenderer));
test(FileTypes.svg, () => checkFile(10, ImageRenderer));
});
test('getter for error', () => {
const el = els[0];
Object.keys(ERROR_STATUSES).forEach((status) => {
el.setState({
isLoading: false,
errorStatus: status,
});
const { actions, ...expectedError } = el.instance().error;
expect(ERROR_STATUSES[status]).toEqual(expectedError);
});
});
});
describe('renderer constraints', () => {
els.forEach((el) => {
const file = el.prop('file');
const fileType = getFileType(file.name);
const RendererComponent = RENDERERS[fileType];
const ActualRendererComponent = jest.requireActual(
'components/FilePreview/BaseRenderers',
)[RendererComponent.name];
test(`${fileType} renderer must have onError and onSuccess props`, () => {
/* eslint-disable react/forbid-foreign-prop-types */
expect(ActualRendererComponent.propTypes.onError).toBeDefined();
expect(ActualRendererComponent.propTypes.onSuccess).toBeDefined();
});
});
});
});

View File

@@ -18,7 +18,6 @@ exports[`File Preview Card component snapshot 1`] = `
>
<div
className="preview-panel"
data-testid="preview-panel"
>
<FileInfo>
<FilePopoverContent

View File

@@ -6,7 +6,6 @@ exports[`File Preview Card component snapshot 1`] = `
overlay={
<Popover
className="overlay-help-popover"
id="file-popover"
>
<Popover.Content>
<h1>
@@ -21,7 +20,7 @@ exports[`File Preview Card component snapshot 1`] = `
<Button
iconAfter={[MockFunction icons.InfoOutline]}
onClick={[MockFunction this.props.onClick]}
size="sm"
size="small"
variant="tertiary"
>
<FormattedMessage

View File

@@ -1,33 +1,292 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`FileRenderer component snapshot is not loading, with error 1`] = `
exports[`FileRenderer component snapshot has error 404 1`] = `
<FileCard
file={
{
"downloadUrl": "file download url",
"name": "filename.txt",
Object {
"description": "file description 0",
"downloadUrl": "/url-path/fake_file_0.pdf",
"name": "fake_file_0.pdf",
}
}
key="file download url"
>
<ErrorBanner
prop="hooks.errorProps"
actions={
Array [
Object {
"id": "retry",
"message": Object {
"defaultMessage": "Retry",
"description": "Retry button for error in file renderer",
"id": "ora-grading.ResponseDisplay.FileRenderer.retryButton",
},
"onClick": [MockFunction this.resetState],
},
]
}
headingMessage={
Object {
"defaultMessage": "File not found",
"description": "File not found error message",
"id": "ora-grading.ResponseDisplay.FileRenderer.fileNotFound",
}
}
>
<FormattedMessage
defaultMessage="File not found"
description="File not found error message"
id="ora-grading.ResponseDisplay.FileRenderer.fileNotFound"
/>
</ErrorBanner>
</FileCard>
`;
exports[`FileRenderer component snapshot has error 500 1`] = `
<FileCard
file={
Object {
"description": "file description 0",
"downloadUrl": "/url-path/fake_file_0.pdf",
"name": "fake_file_0.pdf",
}
}
>
<ErrorBanner
actions={
Array [
Object {
"id": "retry",
"message": Object {
"defaultMessage": "Retry",
"description": "Retry button for error in file renderer",
"id": "ora-grading.ResponseDisplay.FileRenderer.retryButton",
},
"onClick": [MockFunction this.resetState],
},
]
}
headingMessage={
Object {
"defaultMessage": "Unknown errors",
"description": "Unknown errors message",
"id": "ora-grading.ResponseDisplay.FileRenderer.unknownError",
}
}
>
<FormattedMessage
defaultMessage="Unknown errors"
description="Unknown errors message"
id="ora-grading.ResponseDisplay.FileRenderer.unknownError"
/>
</ErrorBanner>
</FileCard>
`;
exports[`FileRenderer component snapshot successful rendering bmp 1`] = `
<FileCard
file={
Object {
"description": "file description 3",
"downloadUrl": "/url-path/fake_file_3.bmp",
"name": "fake_file_3.bmp",
}
}
>
<ImageRenderer
fileName="fake_file_3.bmp"
onError={[MockFunction this.props.onError]}
onSuccess={[MockFunction this.props.onSuccess]}
url="/url-path/fake_file_3.bmp"
/>
</FileCard>
`;
exports[`FileRenderer component snapshot isLoading, no Error 1`] = `
exports[`FileRenderer component snapshot successful rendering gif 1`] = `
<FileCard
file={
{
"downloadUrl": "file download url",
"name": "filename.txt",
Object {
"description": "file description 6",
"downloadUrl": "/url-path/fake_file_6.gif",
"name": "fake_file_6.gif",
}
}
key="file download url"
>
<Renderer
prop="hooks.rendererProps"
<ImageRenderer
fileName="fake_file_6.gif"
onError={[MockFunction this.props.onError]}
onSuccess={[MockFunction this.props.onSuccess]}
url="/url-path/fake_file_6.gif"
/>
</FileCard>
`;
exports[`FileRenderer component snapshot successful rendering jfif 1`] = `
<FileCard
file={
Object {
"description": "file description 7",
"downloadUrl": "/url-path/fake_file_7.jfif",
"name": "fake_file_7.jfif",
}
}
>
<ImageRenderer
fileName="fake_file_7.jfif"
onError={[MockFunction this.props.onError]}
onSuccess={[MockFunction this.props.onSuccess]}
url="/url-path/fake_file_7.jfif"
/>
</FileCard>
`;
exports[`FileRenderer component snapshot successful rendering jpeg 1`] = `
<FileCard
file={
Object {
"description": "file description 2",
"downloadUrl": "/url-path/fake_file_2.jpeg",
"name": "fake_file_2.jpeg",
}
}
>
<ImageRenderer
fileName="fake_file_2.jpeg"
onError={[MockFunction this.props.onError]}
onSuccess={[MockFunction this.props.onSuccess]}
url="/url-path/fake_file_2.jpeg"
/>
</FileCard>
`;
exports[`FileRenderer component snapshot successful rendering jpg 1`] = `
<FileCard
file={
Object {
"description": "file description 1",
"downloadUrl": "/url-path/fake_file_1.jpg",
"name": "fake_file_1.jpg",
}
}
>
<ImageRenderer
fileName="fake_file_1.jpg"
onError={[MockFunction this.props.onError]}
onSuccess={[MockFunction this.props.onSuccess]}
url="/url-path/fake_file_1.jpg"
/>
</FileCard>
`;
exports[`FileRenderer component snapshot successful rendering pdf 1`] = `
<FileCard
file={
Object {
"description": "file description 0",
"downloadUrl": "/url-path/fake_file_0.pdf",
"name": "fake_file_0.pdf",
}
}
>
<PDFRenderer
fileName="fake_file_0.pdf"
onError={[MockFunction this.props.onError]}
onSuccess={[MockFunction this.props.onSuccess]}
url="/url-path/fake_file_0.pdf"
/>
</FileCard>
`;
exports[`FileRenderer component snapshot successful rendering pjp 1`] = `
<FileCard
file={
Object {
"description": "file description 9",
"downloadUrl": "/url-path/fake_file_9.pjp",
"name": "fake_file_9.pjp",
}
}
>
<ImageRenderer
fileName="fake_file_9.pjp"
onError={[MockFunction this.props.onError]}
onSuccess={[MockFunction this.props.onSuccess]}
url="/url-path/fake_file_9.pjp"
/>
</FileCard>
`;
exports[`FileRenderer component snapshot successful rendering pjpeg 1`] = `
<FileCard
file={
Object {
"description": "file description 8",
"downloadUrl": "/url-path/fake_file_8.pjpeg",
"name": "fake_file_8.pjpeg",
}
}
>
<ImageRenderer
fileName="fake_file_8.pjpeg"
onError={[MockFunction this.props.onError]}
onSuccess={[MockFunction this.props.onSuccess]}
url="/url-path/fake_file_8.pjpeg"
/>
</FileCard>
`;
exports[`FileRenderer component snapshot successful rendering png 1`] = `
<FileCard
file={
Object {
"description": "file description 4",
"downloadUrl": "/url-path/fake_file_4.png",
"name": "fake_file_4.png",
}
}
>
<ImageRenderer
fileName="fake_file_4.png"
onError={[MockFunction this.props.onError]}
onSuccess={[MockFunction this.props.onSuccess]}
url="/url-path/fake_file_4.png"
/>
</FileCard>
`;
exports[`FileRenderer component snapshot successful rendering svg 1`] = `
<FileCard
file={
Object {
"description": "file description 10",
"downloadUrl": "/url-path/fake_file_10.svg",
"name": "fake_file_10.svg",
}
}
>
<ImageRenderer
fileName="fake_file_10.svg"
onError={[MockFunction this.props.onError]}
onSuccess={[MockFunction this.props.onSuccess]}
url="/url-path/fake_file_10.svg"
/>
</FileCard>
`;
exports[`FileRenderer component snapshot successful rendering txt 1`] = `
<FileCard
file={
Object {
"description": "file description 5",
"downloadUrl": "/url-path/fake_file_5.txt",
"name": "fake_file_5.txt",
}
}
>
<TXTRenderer
fileName="fake_file_5.txt"
onError={[MockFunction this.props.onError]}
onSuccess={[MockFunction this.props.onSuccess]}
url="/url-path/fake_file_5.txt"
/>
</FileCard>
`;

View File

@@ -1,102 +0,0 @@
import React from 'react';
import { StrictDict } from 'utils';
import { ErrorStatuses } from 'data/constants/requests';
import { FileTypes } from 'data/constants/files';
import {
PDFRenderer,
ImageRenderer,
TXTRenderer,
} from 'components/FilePreview/BaseRenderers';
import * as module from './hooks';
import messages from './messages';
/**
* Config data
*/
export const RENDERERS = StrictDict({
[FileTypes.pdf]: PDFRenderer,
[FileTypes.jpg]: ImageRenderer,
[FileTypes.jpeg]: ImageRenderer,
[FileTypes.bmp]: ImageRenderer,
[FileTypes.png]: ImageRenderer,
[FileTypes.txt]: TXTRenderer,
[FileTypes.gif]: ImageRenderer,
[FileTypes.jfif]: ImageRenderer,
[FileTypes.pjpeg]: ImageRenderer,
[FileTypes.pjp]: ImageRenderer,
[FileTypes.svg]: ImageRenderer,
});
export const SUPPORTED_TYPES = Object.keys(RENDERERS);
export const ERROR_STATUSES = {
[ErrorStatuses.notFound]: messages.fileNotFoundError,
[ErrorStatuses.serverError]: messages.unknownError,
};
/**
* State hooks
*/
export const state = StrictDict({
errorStatus: (val) => React.useState(val),
isLoading: (val) => React.useState(val),
});
/**
* Util methods and transforms
*/
export const getFileType = (fileName) => fileName.split('.').pop()?.toLowerCase();
export const isSupported = (file) => module.SUPPORTED_TYPES.includes(
module.getFileType(file.name),
);
/**
* component hooks
*/
export const renderHooks = ({
file,
intl,
}) => {
const [errorStatus, setErrorStatus] = module.state.errorStatus(null);
const [isLoading, setIsLoading] = module.state.isLoading(true);
const setState = (newState) => {
setErrorStatus(newState.errorStatus);
setIsLoading(newState.isLoading);
};
const stopLoading = (status = null) => setState({ isLoading: false, errorStatus: status });
const errorMessage = (
module.ERROR_STATUSES[errorStatus] || module.ERROR_STATUSES[ErrorStatuses.serverError]
);
const errorAction = {
id: 'retry',
onClick: () => setState({ errorStatus: null, isLoading: true }),
message: messages.retryButton,
};
const error = {
headingMessage: errorMessage,
children: intl.formatMessage(errorMessage),
actions: [errorAction],
};
const Renderer = module.RENDERERS[module.getFileType(file.name)];
const rendererProps = {
fileName: file.name,
url: file.downloadUrl,
onError: stopLoading,
onSuccess: () => stopLoading(),
};
return {
errorStatus,
isLoading,
error,
Renderer,
rendererProps,
};
};

View File

@@ -1,117 +0,0 @@
import { MockUseState, formatMessage } from 'testUtils';
import { keyStore } from 'utils';
import { ErrorStatuses } from 'data/constants/requests';
import * as hooks from './hooks';
const testValue = 'Test-Value';
const state = new MockUseState(hooks);
const hookKeys = keyStore(hooks);
let hook;
describe('FilePreview hooks', () => {
describe('state hooks', () => {
});
describe('non-state hooks', () => {
beforeEach(() => {
state.mock();
});
afterEach(() => {
state.restore();
});
describe('utility methods', () => {
describe('getFileType', () => {
it('returns file extension if available, in lowercase', () => {
expect(hooks.getFileType('thing.TXT')).toEqual('txt');
expect(hooks.getFileType(testValue)).toEqual(testValue.toLowerCase());
});
});
describe('isSupported', () => {
it('returns true iff the filetype is included in SUPPORTED_TYPES', () => {
let spy = jest.spyOn(hooks, hookKeys.getFileType).mockImplementationOnce(v => v);
expect(hooks.isSupported({ name: hooks.SUPPORTED_TYPES[0] })).toEqual(true);
spy = jest.spyOn(hooks, hookKeys.getFileType).mockImplementationOnce(v => v);
expect(hooks.isSupported({ name: testValue })).toEqual(false);
spy.mockRestore();
});
});
});
describe('component hooks', () => {
describe('renderHooks', () => {
const file = {
name: 'test-file-name.txt',
downloadUrl: 'my-test-download-url.jpg',
};
beforeEach(() => {
hook = hooks.renderHooks({ intl: { formatMessage }, file });
});
describe('returned object', () => {
test('errorStatus and isLoading tied to state, initialized to null and true', () => {
expect(hook.errorStatus).toEqual(state.stateVals.errorStatus);
expect(hook.errorStatus).toEqual(null);
expect(hook.isLoading).toEqual(state.stateVals.isLoading);
expect(hook.isLoading).toEqual(true);
});
describe('error', () => {
it('loads message from current error status, if valid, else from serverError', () => {
expect(hook.error.headingMessage).toEqual(
hooks.ERROR_STATUSES[ErrorStatuses.serverError],
);
expect(hook.error.children).toEqual(
formatMessage(hooks.ERROR_STATUSES[ErrorStatuses.serverError]),
);
state.mockVal(state.keys.errorStatus, ErrorStatuses.notFound);
hook = hooks.renderHooks({ intl: { formatMessage }, file });
expect(hook.error.headingMessage).toEqual(
hooks.ERROR_STATUSES[ErrorStatuses.notFound],
);
expect(hook.error.children).toEqual(
formatMessage(hooks.ERROR_STATUSES[ErrorStatuses.notFound]),
);
});
it('provides a single action', () => {
expect(hook.error.actions.length).toEqual(1);
});
describe('action', () => {
it('sets errorState to null and isLoading to true on click', () => {
hook.error.actions[0].onClick();
expect(state.setState.isLoading).toHaveBeenCalledWith(true);
expect(state.setState.errorStatus).toHaveBeenCalledWith(null);
});
});
});
describe('Renderer', () => {
it('returns configured renderer based on filetype', () => {
hooks.SUPPORTED_TYPES.forEach(type => {
jest.spyOn(hooks, hookKeys.getFileType).mockReturnValueOnce(type);
hook = hooks.renderHooks({ intl: { formatMessage }, file });
expect(hook.Renderer).toEqual(hooks.RENDERERS[type]);
});
});
});
describe('rendererProps', () => {
it('forwards url and fileName from file', () => {
expect(hook.rendererProps.fileName).toEqual(file.name);
expect(hook.rendererProps.url).toEqual(file.downloadUrl);
});
describe('onError', () => {
it('it sets isLoading to false and loads errorStatus', () => {
hook.rendererProps.onError(testValue);
expect(state.setState.isLoading).toHaveBeenCalledWith(false);
expect(state.setState.errorStatus).toHaveBeenCalledWith(testValue);
});
});
describe('onSuccess', () => {
it('it sets isLoading to false and errorStatus to null', () => {
hook.rendererProps.onSuccess(testValue);
expect(state.setState.isLoading).toHaveBeenCalledWith(false);
expect(state.setState.errorStatus).toHaveBeenCalledWith(null);
});
});
});
});
});
});
});
});

View File

@@ -1,2 +1 @@
export { default as FileRenderer } from './FileRenderer';
export { isSupported } from './hooks';
export { default as FileRenderer, isSupported } from './FileRenderer';

View File

@@ -1,14 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Head snapshot 1`] = `
<Helmet>
<title>
ORA staff grading | site-name
</title>
<link
href="favicon-url"
rel="shortcut icon"
type="image/x-icon"
/>
</Helmet>
`;

View File

@@ -1,20 +0,0 @@
import React from 'react';
import { Helmet } from 'react-helmet';
import { useIntl } from '@edx/frontend-platform/i18n';
import { getConfig } from '@edx/frontend-platform';
import messages from './messages';
const Head = () => {
const { formatMessage } = useIntl();
return (
<Helmet>
<title>
{formatMessage(messages.PageTitle, { siteName: getConfig().SITE_NAME })}
</title>
<link rel="shortcut icon" href={getConfig().FAVICON_URL} type="image/x-icon" />
</Helmet>
);
};
export default Head;

View File

@@ -1,25 +0,0 @@
import React from 'react';
import { getConfig } from '@edx/frontend-platform';
import { shallow } from '@edx/react-unit-test-utils';
import Head from '.';
jest.mock('react-helmet', () => ({
Helmet: 'Helmet',
}));
jest.mock('@edx/frontend-platform', () => ({
getConfig: () => ({
SITE_NAME: 'site-name',
FAVICON_URL: 'favicon-url',
}),
}));
describe('Head', () => {
it('snapshot', () => {
const el = shallow(<Head />);
expect(el.snapshot).toMatchSnapshot();
expect(el.instance.findByType('title')[0].el.children[0]).toContain(getConfig().SITE_NAME);
expect(el.instance.findByType('link')[0].props.href).toEqual(getConfig().FAVICON_URL);
});
});

View File

@@ -1,11 +0,0 @@
import { defineMessages } from '@edx/frontend-platform/i18n';
const messages = defineMessages({
PageTitle: {
id: 'PageTitle',
defaultMessage: 'ORA staff grading | {siteName}',
description: 'Title tag',
},
});
export default messages;

View File

@@ -6,7 +6,6 @@ exports[`Info Popover Component snapshot 1`] = `
overlay={
<Popover
className="overlay-help-popover"
id="info-popover"
>
<Popover.Content>
<div>
@@ -21,7 +20,6 @@ exports[`Info Popover Component snapshot 1`] = `
<IconButton
alt="Display more info"
className="esg-help-icon"
data-testid="esg-help-icon"
iconAs="Icon"
onClick={[MockFunction this.props.onClick]}
src={[MockFunction icons.InfoOutline]}

View File

@@ -6,12 +6,10 @@ import {
Popover,
Icon,
IconButton,
} from '@openedx/paragon';
import { InfoOutline } from '@openedx/paragon/icons';
} from '@edx/paragon';
import { InfoOutline } from '@edx/paragon/icons';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { nullMethod } from 'hooks';
import messages from './messages';
/**
@@ -23,14 +21,13 @@ export const InfoPopover = ({ onClick, children, intl }) => (
placement="right-end"
flip
overlay={(
<Popover id="info-popover" className="overlay-help-popover">
<Popover className="overlay-help-popover">
<Popover.Content>{children}</Popover.Content>
</Popover>
)}
>
<IconButton
className="esg-help-icon"
data-testid="esg-help-icon"
src={InfoOutline}
alt={intl.formatMessage(messages.altText)}
iconAs={Icon}
@@ -40,7 +37,7 @@ export const InfoPopover = ({ onClick, children, intl }) => (
);
InfoPopover.defaultProps = {
onClick: nullMethod,
onClick: () => {},
};
InfoPopover.propTypes = {
onClick: PropTypes.func,

View File

@@ -1,5 +1,5 @@
import React from 'react';
import { shallow } from '@edx/react-unit-test-utils';
import { shallow } from 'enzyme';
import { formatMessage } from 'testUtils';
import { InfoPopover } from '.';
@@ -12,12 +12,12 @@ describe('Info Popover Component', () => {
el = shallow(<InfoPopover onClick={onClick} intl={{ formatMessage }}>{child}</InfoPopover>);
});
test('snapshot', () => {
expect(el.snapshot).toMatchSnapshot();
expect(el).toMatchSnapshot();
});
describe('Component', () => {
test('Test component render', () => {
expect(el.instance.children.length).toEqual(1);
expect(el.instance.findByTestId('esg-help-icon').length).toEqual(1);
expect(el.length).toEqual(1);
expect(el.find('.esg-help-icon').length).toEqual(1);
});
});
});

View File

@@ -1,7 +1,7 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Spinner } from '@openedx/paragon';
import { Spinner } from '@edx/paragon';
import { FormattedMessage } from '@edx/frontend-platform/i18n';
/**

View File

@@ -1,32 +1,24 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Badge } from '@openedx/paragon';
import { Badge } from '@edx/paragon';
import { FormattedMessage } from '@edx/frontend-platform/i18n';
import { StrictDict } from 'utils';
import { gradingStatuses as statuses } from 'data/services/lms/constants';
import messages from 'data/services/lms/messages';
export const buttonVariants = StrictDict({
primary: 'primary',
light: 'light',
success: 'success',
warning: 'warning',
});
export const statusVariants = StrictDict({
[statuses.ungraded]: buttonVariants.primary,
[statuses.locked]: buttonVariants.light,
[statuses.graded]: buttonVariants.success,
[statuses.inProgress]: buttonVariants.warning,
});
export const statusVariants = {
[statuses.ungraded]: 'primary',
[statuses.locked]: 'light',
[statuses.graded]: 'success',
[statuses.inProgress]: 'warning',
};
/**
* <StatusBadge />
*/
export const StatusBadge = ({ className, status }) => {
if (!Object.keys(statusVariants).includes(status)) {
if (statusVariants[status] === undefined) {
return null;
}
return (

View File

@@ -1,35 +0,0 @@
import React from 'react';
import { shallow } from '@edx/react-unit-test-utils';
import { gradingStatuses } from 'data/services/lms/constants';
import { StatusBadge } from './StatusBadge';
const className = 'test-className';
describe('StatusBadge component', () => {
const render = (status) => shallow(<StatusBadge className={className} status={status} />);
describe('behavior', () => {
it('does not render if status does not have configured variant', () => {
const el = render('arbitrary');
expect(el.snapshot).toMatchSnapshot();
expect(el.isEmptyRender()).toEqual(true);
});
describe('status snapshots: loads badge with configured variant and message.', () => {
test('`ungraded` shows primary button variant and message', () => {
const el = render(gradingStatuses.ungraded);
expect(el.snapshot).toMatchSnapshot();
});
test('`locked` shows light button variant and message', () => {
const el = render(gradingStatuses.locked);
expect(el.snapshot).toMatchSnapshot();
});
test('`graded` shows success button variant and message', () => {
const el = render(gradingStatuses.graded);
expect(el.snapshot).toMatchSnapshot();
});
test('`inProgress` shows warning button variant and message', () => {
const el = render(gradingStatuses.inProgress);
expect(el.snapshot).toMatchSnapshot();
});
});
});
});

View File

@@ -20,7 +20,7 @@ exports[`ConfirmModal snapshot: closed 1`] = `
</ActionRow>
}
isOpen={false}
onClose={[MockFunction hooks.nullMethod]}
onClose={[Function]}
title="test-title"
>
<p>
@@ -49,7 +49,7 @@ exports[`ConfirmModal snapshot: open 1`] = `
</ActionRow>
}
isOpen={true}
onClose={[MockFunction hooks.nullMethod]}
onClose={[Function]}
title="test-title"
>
<p>

View File

@@ -1,55 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`StatusBadge component behavior does not render if status does not have configured variant 1`] = `null`;
exports[`StatusBadge component behavior status snapshots: loads badge with configured variant and message. \`graded\` shows success button variant and message 1`] = `
<Badge
className="test-className"
variant="success"
>
<FormattedMessage
defaultMessage="Grading Completed"
description="Grading status label for graded submission"
id="ora-grading.lms-api.gradingStatusDisplay.graded"
/>
</Badge>
`;
exports[`StatusBadge component behavior status snapshots: loads badge with configured variant and message. \`inProgress\` shows warning button variant and message 1`] = `
<Badge
className="test-className"
variant="warning"
>
<FormattedMessage
defaultMessage="You are currently grading this response"
description="Grading status label for in-progress submission"
id="ora-grading.lms-api.gradingStatusDisplay.inProgress"
/>
</Badge>
`;
exports[`StatusBadge component behavior status snapshots: loads badge with configured variant and message. \`locked\` shows light button variant and message 1`] = `
<Badge
className="test-className"
variant="light"
>
<FormattedMessage
defaultMessage="Currently being graded by someone else"
description="Grading status label for locked submission"
id="ora-grading.lms-api.gradingStatusDisplay.locked"
/>
</Badge>
`;
exports[`StatusBadge component behavior status snapshots: loads badge with configured variant and message. \`ungraded\` shows primary button variant and message 1`] = `
<Badge
className="test-className"
variant="primary"
>
<FormattedMessage
defaultMessage="Ungraded"
description="Grading status label for ungraded submission"
id="ora-grading.lms-api.gradingStatusDisplay.ungraded"
/>
</Badge>
`;

16
src/config/index.js Normal file
View File

@@ -0,0 +1,16 @@
const configuration = {
// BASE_URL: process.env.BASE_URL,
LMS_BASE_URL: process.env.LMS_BASE_URL,
// LOGIN_URL: process.env.LOGIN_URL,
// LOGOUT_URL: process.env.LOGOUT_URL,
// CSRF_TOKEN_API_PATH: process.env.CSRF_TOKEN_API_PATH,
// REFRESH_ACCESS_TOKEN_ENDPOINT: process.env.REFRESH_ACCESS_TOKEN_ENDPOINT,
// DATA_API_BASE_URL: process.env.DATA_API_BASE_URL,
// SECURE_COOKIES: process.env.NODE_ENV !== 'development',
// SEGMENT_KEY: process.env.SEGMENT_KEY,
// ACCESS_TOKEN_COOKIE_NAME: process.env.ACCESS_TOKEN_COOKIE_NAME,
};
const features = {};
export { configuration, features };

View File

@@ -1,11 +0,0 @@
import React from 'react';
import { shallow } from '@edx/react-unit-test-utils';
import { CTA } from '.';
describe('CTA component', () => {
test('snapshots', () => {
const el = shallow(<CTA hide />);
expect(el.snapshot).toMatchSnapshot();
});
});

View File

@@ -1,31 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`CTA component snapshots 1`] = `
<PageBanner>
<span>
<FormattedMessage
defaultMessage="Thanks for using the new ORA staff grading experience. "
description="Thank user for using ora and ask for feed back"
id="ora-grading.CTA.feedbackMessage"
/>
<Hyperlink
destination="https://docs.google.com/forms/d/1Hu1rgJcCHl5_EtDb5Up3hiZ40sSUtkZQfRHJ3fWOvfQ/edit"
isInline={true}
showLaunchIcon={false}
target="_blank"
variant="muted"
>
<FormattedMessage
defaultMessage="Provide some feedback"
description="placeholder for the feedback anchor link"
id="ora-grading.CTA.linkMessage"
/>
</Hyperlink>
<FormattedMessage
defaultMessage=" and let us know what you think!"
description="inform user to provide feedback"
id="ora-grading.CTA.letUsKnowMessage"
/>
</span>
</PageBanner>
`;

View File

@@ -1,29 +0,0 @@
import React from 'react';
import { FormattedMessage } from '@edx/frontend-platform/i18n';
import { PageBanner, Hyperlink } from '@openedx/paragon';
import messages from './messages';
/**
* <CTA />
*/
export const CTA = () => (
<PageBanner>
<span>
<FormattedMessage {...messages.ctaFeedbackMessage} />
<Hyperlink
isInline
variant="muted"
destination="https://docs.google.com/forms/d/1Hu1rgJcCHl5_EtDb5Up3hiZ40sSUtkZQfRHJ3fWOvfQ/edit"
target="_blank"
showLaunchIcon={false}
>
<FormattedMessage {...messages.ctaLinkMessage} />
</Hyperlink>
<FormattedMessage {...messages.ctaLetUsKnowMessage} />
</span>
</PageBanner>
);
export default CTA;

View File

@@ -1,23 +0,0 @@
/* eslint-disable quotes */
import { defineMessages } from '@edx/frontend-platform/i18n';
import { StrictDict } from 'utils';
const messages = defineMessages({
ctaFeedbackMessage: {
id: 'ora-grading.CTA.feedbackMessage',
defaultMessage: 'Thanks for using the new ORA staff grading experience. ',
description: 'Thank user for using ora and ask for feed back',
},
ctaLinkMessage: {
id: 'ora-grading.CTA.linkMessage',
defaultMessage: 'Provide some feedback',
description: 'placeholder for the feedback anchor link',
},
ctaLetUsKnowMessage: {
id: 'ora-grading.CTA.letUsKnowMessage',
defaultMessage: ' and let us know what you think!',
description: 'inform user to provide feedback',
},
});
export default StrictDict(messages);

View File

@@ -0,0 +1,38 @@
import React from 'react';
import { getConfig } from '@edx/frontend-platform';
import { getLoginRedirectUrl } from '@edx/frontend-platform/auth';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { Button } from '@edx/paragon';
import message from './messages';
export const getRegisterUrl = () => {
const { LMS_BASE_URL } = getConfig();
const locationHref = encodeURIComponent(global.location.href);
return `${LMS_BASE_URL}/register?next=${locationHref}`;
};
export const AnonymousUserMenu = ({ intl }) => (
<div>
<Button
className="mr-3"
variant="outline-primary"
href={getRegisterUrl()}
>
{intl.formatMessage(message.registerSentenceCase)}
</Button>
<Button
variant="primary"
href={`${getLoginRedirectUrl(global.location.href)}`}
>
{intl.formatMessage(message.signInSentenceCase)}
</Button>
</div>
);
AnonymousUserMenu.propTypes = {
intl: intlShape.isRequired,
};
export default injectIntl(AnonymousUserMenu);

View File

@@ -0,0 +1,24 @@
import React from 'react';
import { shallow } from 'enzyme';
import { AnonymousUserMenu } from './AnonymousUserMenu';
jest.mock('@edx/frontend-platform', () => ({
getConfig: () => ({
LMS_BASE_URL: '<LMS_BASE_URL>',
}),
}));
jest.mock('@edx/frontend-platform/auth', () => ({
getLoginRedirectUrl: (url) => `redirect:${url}`,
}));
describe('Header AnonymousUserMenu component', () => {
const props = {
intl: { formatMessage: (msg) => msg.defaultMessage },
};
test('snapshot', () => {
expect(
shallow(<AnonymousUserMenu {...props} />),
).toMatchSnapshot();
});
});

View File

@@ -0,0 +1,27 @@
import React from 'react';
import PropTypes from 'prop-types';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faUserCircle } from '@fortawesome/free-solid-svg-icons';
import { Dropdown } from '@edx/paragon';
export const UserAvatar = ({ username }) => (
<Dropdown.Toggle variant="outline-primary">
<FontAwesomeIcon
icon={faUserCircle}
className="d-md-none"
size="lg"
/>
<span data-hj-suppress className="d-none d-md-inline">
{username}
</span>
</Dropdown.Toggle>
);
UserAvatar.propTypes = {
username: PropTypes.string.isRequired,
};
UserAvatar.defaultProps = {};
export default UserAvatar;

View File

@@ -0,0 +1,23 @@
import React from 'react';
import { shallow } from 'enzyme';
import UserAvatar from './UserAvatar';
jest.mock('@edx/frontend-platform', () => ({
getConfig: () => ({
LMS_BASE_URL: '<LMS_BASE_URL>',
LOGOUT_URL: '<LOGOUT_URL>',
SUPPORT_URL: '<SUPPORT_URL>',
}),
}));
describe('Header AuthenticatedUserDropdown UserAvatar component', () => {
const props = {
username: 'test-username',
};
test('snapshot', () => {
expect(
shallow(<UserAvatar {...props} />),
).toMatchSnapshot();
});
});

View File

@@ -0,0 +1,40 @@
import React from 'react';
import PropTypes from 'prop-types';
import { getConfig } from '@edx/frontend-platform';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { Dropdown } from '@edx/paragon';
import messages from '../messages';
export class UserMenu extends React.Component {
menuItem(href, message) {
return (
<Dropdown.Item href={href}>
{this.props.intl.formatMessage(message)}
</Dropdown.Item>
);
}
render() {
const { username } = this.props;
const { LMS_BASE_URL, LOGOUT_URL } = getConfig();
return (
<Dropdown.Menu className="dropdown-menu-right">
{this.menuItem(`${LMS_BASE_URL}/dashboard`, messages.dashboard)}
{this.menuItem(`${LMS_BASE_URL}/u/${username}`, messages.profile)}
{this.menuItem(`${LMS_BASE_URL}/account/settings`, messages.account)}
{this.menuItem(LOGOUT_URL, messages.signOut)}
</Dropdown.Menu>
);
}
}
UserMenu.propTypes = {
intl: intlShape.isRequired,
username: PropTypes.string.isRequired,
};
UserMenu.defaultProps = {};
export default injectIntl(UserMenu);

View File

@@ -0,0 +1,24 @@
import React from 'react';
import { shallow } from 'enzyme';
import { UserMenu } from './UserMenu';
jest.mock('@edx/frontend-platform', () => ({
getConfig: () => ({
LMS_BASE_URL: '<LMS_BASE_URL>',
LOGOUT_URL: '<LOGOUT_URL>',
SUPPORT_URL: '<SUPPORT_URL>',
}),
}));
describe('Header AuthenticatedUserDropdown UserMenu component', () => {
const props = {
intl: { formatMessage: (msg) => msg.defaultMessage },
username: 'test-username',
};
test('snapshot', () => {
expect(
shallow(<UserMenu {...props} />),
).toMatchSnapshot();
});
});

View File

@@ -0,0 +1,31 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Header AuthenticatedUserDropdown UserAvatar component snapshot 1`] = `
<Dropdown.Toggle
variant="outline-primary"
>
<FontAwesomeIcon
className="d-md-none"
icon={
Object {
"icon": Array [
496,
512,
Array [],
"f2bd",
"M248 8C111 8 0 119 0 256s111 248 248 248 248-111 248-248S385 8 248 8zm0 96c48.6 0 88 39.4 88 88s-39.4 88-88 88-88-39.4-88-88 39.4-88 88-88zm0 344c-58.7 0-111.3-26.6-146.5-68.2 18.8-35.4 55.6-59.8 98.5-59.8 2.4 0 4.8.4 7.1 1.1 13 4.2 26.6 6.9 40.9 6.9 14.3 0 28-2.7 40.9-6.9 2.3-.7 4.7-1.1 7.1-1.1 42.9 0 79.7 24.4 98.5 59.8C359.3 421.4 306.7 448 248 448z",
],
"iconName": "user-circle",
"prefix": "fas",
}
}
size="lg"
/>
<span
className="d-none d-md-inline"
data-hj-suppress={true}
>
test-username
</span>
</Dropdown.Toggle>
`;

View File

@@ -0,0 +1,28 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Header AuthenticatedUserDropdown UserMenu component snapshot 1`] = `
<Dropdown.Menu
className="dropdown-menu-right"
>
<Dropdown.Item
href="<LMS_BASE_URL>/dashboard"
>
Dashboard
</Dropdown.Item>
<Dropdown.Item
href="<LMS_BASE_URL>/u/test-username"
>
Profile
</Dropdown.Item>
<Dropdown.Item
href="<LMS_BASE_URL>/account/settings"
>
Account
</Dropdown.Item>
<Dropdown.Item
href="<LOGOUT_URL>"
>
Sign Out
</Dropdown.Item>
</Dropdown.Menu>
`;

View File

@@ -0,0 +1,22 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Header AuthenticatedUserDropdown component snapshot 1`] = `
<Fragment>
<a
className="text-gray-700 mr-3"
href="<SUPPORT_URL>"
>
Help
</a>
<Dropdown
className="user-dropdown"
>
<UserAvatar
username="test-username"
/>
<UserMenu
username="test-username"
/>
</Dropdown>
</Fragment>
`;

View File

@@ -0,0 +1,35 @@
import React from 'react';
import PropTypes from 'prop-types';
import { getConfig } from '@edx/frontend-platform';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { Dropdown } from '@edx/paragon';
import UserMenu from './UserMenu';
import UserAvatar from './UserAvatar';
import messages from '../messages';
export const AuthenticatedUserDropdown = ({
intl,
username,
}) => (
<>
<a className="text-gray-700 mr-3" href={`${getConfig().SUPPORT_URL}`}>
{intl.formatMessage(messages.help)}
</a>
<Dropdown className="user-dropdown">
<UserAvatar username={username} />
<UserMenu username={username} />
</Dropdown>
</>
);
AuthenticatedUserDropdown.propTypes = {
intl: intlShape.isRequired,
username: PropTypes.string.isRequired,
};
AuthenticatedUserDropdown.defaultProps = {};
export default injectIntl(AuthenticatedUserDropdown);

View File

@@ -0,0 +1,24 @@
import React from 'react';
import { shallow } from 'enzyme';
import { AuthenticatedUserDropdown } from '.';
jest.mock('@edx/frontend-platform', () => ({
getConfig: () => ({
SUPPORT_URL: '<SUPPORT_URL>',
}),
}));
jest.mock('./UserAvatar', () => 'UserAvatar');
jest.mock('./UserMenu', () => 'UserMenu');
describe('Header AuthenticatedUserDropdown component', () => {
const props = {
intl: { formatMessage: (msg) => msg.defaultMessage },
username: 'test-username',
};
test('snapshot', () => {
expect(
shallow(<AuthenticatedUserDropdown {...props} />),
).toMatchSnapshot();
});
});

View File

@@ -0,0 +1,32 @@
import React from 'react';
import PropTypes from 'prop-types';
export const CourseLabel = ({
courseOrg,
courseNumber,
courseTitle,
}) => (
<div
className="flex-grow-1 course-title-lockup"
style={{ lineHeight: 1 }}
>
<span className="d-block small m-0">
{courseOrg} {courseNumber}
</span>
<span className="d-block m-0 font-weight-bold course-title">
{courseTitle}
</span>
</div>
);
CourseLabel.propTypes = {
courseOrg: PropTypes.string,
courseNumber: PropTypes.string,
courseTitle: PropTypes.string,
};
CourseLabel.defaultProps = {
courseOrg: null,
courseNumber: null,
courseTitle: null,
};
export default CourseLabel;

View File

@@ -0,0 +1,18 @@
import React from 'react';
import { shallow } from 'enzyme';
import CourseLabel from './CourseLabel';
const courseData = {
courseOrg: 'course-org',
courseNumber: 'course-number',
courseTitle: 'course-title',
};
describe('Header CourseLabel component', () => {
test('snapshot', () => {
expect(
shallow(<CourseLabel {...courseData} />),
).toMatchSnapshot();
});
});

View File

@@ -0,0 +1,17 @@
import React from 'react';
import { getConfig } from '@edx/frontend-platform';
const LinkedLogo = () => (
<a
className="logo"
href={`${getConfig().LMS_BASE_URL}/dashboard`}
>
<img
className="d-block"
src={getConfig().LOGO_URL}
alt={getConfig().SITE_NAME}
/>
</a>
);
export default LinkedLogo;

View File

@@ -0,0 +1,20 @@
import React from 'react';
import { shallow } from 'enzyme';
import LinkedLogo from './LinkedLogo';
jest.mock('@edx/frontend-platform', () => ({
getConfig: () => ({
LMS_BASE_URL: '<getConfig().LMS_BASE_URL>',
LOGO_URL: '<getConfig().LOGO_URL>',
SITE_NAME: '<getConfig().SITE_NAME>',
}),
}));
describe('Header CourseLabel component', () => {
test('snapshot', () => {
expect(
shallow(<LinkedLogo />),
).toMatchSnapshot();
});
});

View File

@@ -0,0 +1,19 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Header AnonymousUserMenu component snapshot 1`] = `
<div>
<Button
className="mr-3"
href="<LMS_BASE_URL>/register?next=http%3A%2F%2Flocalhost%2F"
variant="outline-primary"
>
Register
</Button>
<Button
href="redirect:http://localhost/"
variant="primary"
>
Sign in
</Button>
</div>
`;

View File

@@ -0,0 +1,25 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Header CourseLabel component snapshot 1`] = `
<div
className="flex-grow-1 course-title-lockup"
style={
Object {
"lineHeight": 1,
}
}
>
<span
className="d-block small m-0"
>
course-org
course-number
</span>
<span
className="d-block m-0 font-weight-bold course-title"
>
course-title
</span>
</div>
`;

View File

@@ -0,0 +1,14 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Header CourseLabel component snapshot 1`] = `
<a
className="logo"
href="<getConfig().LMS_BASE_URL>/dashboard"
>
<img
alt="<getConfig().SITE_NAME>"
className="d-block"
src="<getConfig().LOGO_URL>"
/>
</a>
`;

View File

@@ -0,0 +1,51 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Header component snapshot 1`] = `
<header
className="course-header"
>
<a
className="sr-only sr-only-focusable"
href="#main-content"
>
Skip to main content.
</a>
<div
className="container-xl py-2 d-flex align-items-center"
>
<LinkedLogo />
<CourseLabel
courseNumber="course-number"
courseOrg="course-org"
courseTitle="course-title"
/>
<AnonymousUserMenu />
</div>
</header>
`;
exports[`Header component snapshot with authenticatedUser 1`] = `
<header
className="course-header"
>
<a
className="sr-only sr-only-focusable"
href="#main-content"
>
Skip to main content.
</a>
<div
className="container-xl py-2 d-flex align-items-center"
>
<LinkedLogo />
<CourseLabel
courseNumber="course-number"
courseOrg="course-org"
courseTitle="course-title"
/>
<AuthenticatedUserDropdown
username="test"
/>
</div>
</header>
`;

View File

@@ -0,0 +1,47 @@
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 AnonymousUserMenu from './AnonymousUserMenu';
import AuthenticatedUserDropdown from './AuthenticatedUserDropdown';
import LinkedLogo from './LinkedLogo';
import CourseLabel from './CourseLabel';
import messages from './messages';
export const Header = ({
courseOrg,
courseNumber,
courseTitle,
intl,
}) => {
const { authenticatedUser } = useContext(AppContext);
return (
<header className="course-header">
<a className="sr-only sr-only-focusable" href="#main-content">
{intl.formatMessage(messages.skipNavLink)}
</a>
<div className="container-xl py-2 d-flex align-items-center">
<LinkedLogo />
<CourseLabel {...{ courseOrg, courseNumber, courseTitle }} />
{authenticatedUser
? (<AuthenticatedUserDropdown username={authenticatedUser.username} />)
: (<AnonymousUserMenu />)}
</div>
</header>
);
};
Header.propTypes = {
courseOrg: PropTypes.string,
courseNumber: PropTypes.string,
courseTitle: PropTypes.string,
intl: intlShape.isRequired,
};
Header.defaultProps = {
courseOrg: null,
courseNumber: null,
courseTitle: null,
};
export default injectIntl(Header);

Some files were not shown because too many files have changed in this diff Show More