Compare commits

..

1 Commits

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

2
.env
View File

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

View File

@@ -36,5 +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=''

View File

@@ -4,14 +4,8 @@ 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

@@ -11,16 +11,17 @@ on:
jobs:
tests:
runs-on: ubuntu-20.04
strategy:
matrix:
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@v3
# 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: 18.15
node-version: ${{ matrix.node }}
- name: Install dependencies
run: npm ci
@@ -38,7 +39,7 @@ jobs:
run: npm run build
- name: Run Coverage
uses: codecov/codecov-action@v3
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

@@ -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

1
.nvmrc
View File

@@ -1 +0,0 @@
18.17

View File

@@ -3,17 +3,18 @@ npm-install-%: ## install specified % npm package
git add package.json
transifex_resource = frontend-app-ora-grading
transifex_langs = "ar,de_DE,es_419,fa_IR,fr,fr_CA,hi,it_IT,pt_PT,uk,ru,zh_CN"
transifex_langs = "ar,fr,es_419,zh_CN"
intl_imports = ./node_modules/.bin/intl-imports.js
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-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
@@ -48,30 +49,15 @@ push_translations:
# Pushing strings to Transifex...
tx push -s
# Fetching hashes from Transifex...
./node_modules/@edx/reactifex/bash_scripts/get_hashed_strings_v3.sh
./node_modules/reactifex/bash_scripts/get_hashed_strings.sh $(tx_url1)
# Writing out comments to file...
$(transifex_utils) $(transifex_temp) --comments --v3-scripts-path
$(transifex_utils) $(transifex_temp) --comments
# Pushing comments to Transifex...
./node_modules/@edx/reactifex/bash_scripts/put_comments_v3.sh
./node_modules/reactifex/bash_scripts/put_comments.sh $(tx_url2)
ifeq ($(OPENEDX_ATLAS_PULL),)
# Pulls translations from Transifex.
pull_translations:
tx pull -t -f --mode reviewed --languages=$(transifex_langs)
else
# Experimental: OEP-58 Pulls translations using atlas
pull_translations:
rm -rf src/i18n/messages
mkdir src/i18n/messages
cd src/i18n/messages \
&& atlas pull --filter=$(transifex_langs) \
translations/frontend-component-footer/src/i18n/messages:frontend-component-footer \
translations/frontend-component-header/src/i18n/messages:frontend-component-header \
translations/paragon/src/i18n/messages:paragon \
translations/frontend-app-ora-grading/src/i18n/messages:frontend-app-ora-grading
$(intl_imports) frontend-component-footer frontend-component-header paragon frontend-app-ora-grading
endif
tx pull -f --mode reviewed --languages=$(transifex_langs)
# This target is used by CI.
validate-no-uncommitted-package-lock-changes:

View File

@@ -1,233 +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
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:content-aurora
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

@@ -12,9 +12,6 @@ module.exports = createConfig('jest', {
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',

51884
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -8,6 +8,8 @@
},
"scripts": {
"build": "fedx-scripts webpack",
"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/",
@@ -24,22 +26,21 @@
"access": "public"
},
"dependencies": {
"@edx/brand": "npm:@edx/brand-openedx@^1.2.0",
"@edx/frontend-component-footer": "12.2.1",
"@edx/frontend-component-header": "4.6.0",
"@edx/frontend-platform": "4.6.1",
"@edx/paragon": "^20.44.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",
"@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.27.0",
"axios": "^0.21.4",
"classnames": "^2.3.1",
"core-js": "3.32.1",
"core-js": "3.16.2",
"dompurify": "^2.3.1",
"email-prop-type": "^3.0.1",
"enzyme": "^3.11.0",
@@ -47,48 +48,46 @@
"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",
"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": "5.3.4",
"react-router-dom": "5.3.4",
"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.2.0",
"@edx/frontend-build": "^12.7.0",
"@edx/reactifex": "^2.1.1",
"@edx/frontend-build": "^9.1.4",
"@testing-library/jest-dom": "^5.14.1",
"@testing-library/react": "12.1.5",
"@wojtekmaj/enzyme-adapter-react-17": "0.8.0",
"@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.6.4",
"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

@@ -4,28 +4,24 @@ import { connect } from 'react-redux';
import { BrowserRouter as Router } from 'react-router-dom';
import Footer from '@edx/frontend-component-footer';
import { LearningHeader as Header } from '@edx/frontend-component-header';
import { selectors } from 'data/redux';
import DemoWarning from 'containers/DemoWarning';
import CTA from 'containers/CTA';
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}
/>
{!isEnabled && <DemoWarning />}
<CTA />
<main>
<ListView />
</main>

View File

@@ -10,7 +10,6 @@ $fa-font-path: "~font-awesome/fonts";
$input-focus-box-shadow: $input-box-shadow; // hack to get upgrade to paragon 4.0.0 to work
@import "~@edx/frontend-component-footer/dist/_footer";
@import "~@edx/frontend-component-header/dist/index";
#root {
display: flex;
@@ -43,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

@@ -2,7 +2,6 @@ import React from 'react';
import { shallow } from 'enzyme';
import Footer from '@edx/frontend-component-footer';
import { LearningHeader as Header } from '@edx/frontend-component-header';
import ListView from 'containers/ListView';
@@ -17,15 +16,11 @@ jest.mock('data/redux', () => ({
},
}));
jest.mock('@edx/frontend-component-header', () => ({
LearningHeader: 'Header',
}));
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;
@@ -62,16 +57,5 @@ describe('App router component', () => {
test('Footer logo drawn from env variable', () => {
expect(router.find(Footer).props().logo).toEqual(logo);
});
test('Header to use courseMetadata props', () => {
const {
courseTitle,
courseNumber,
courseOrg,
} = router.find(Header).props();
expect(courseTitle).toEqual(props.courseMetadata.title);
expect(courseNumber).toEqual(props.courseMetadata.number);
expect(courseOrg).toEqual(props.courseMetadata.org);
});
});
});

View File

@@ -3,14 +3,12 @@
exports[`App router component snapshot: disabled (show demo warning) 1`] = `
<BrowserRouter>
<div>
<Head />
<Header
<CourseHeader
courseNumber="course-number"
courseOrg="course-org"
courseTitle="course-title"
/>
<DemoWarning />
<CTA />
<main>
<ListView />
</main>
@@ -24,13 +22,11 @@ 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"
/>
<CTA />
<main>
<ListView />
</main>

View File

@@ -1,23 +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={
Object {
"dispatch": [Function],
"getState": [Function],
"replaceReducer": [Function],
"subscribe": [Function],
Symbol(Symbol.observable): [Function],
}
}
>
<App />
</AppProvider>
`;

View File

@@ -2,7 +2,6 @@ import React from 'react';
import PropTypes from 'prop-types';
import { AlertModal, ActionRow, Button } from '@edx/paragon';
import { nullMethod } from 'hooks';
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

@@ -19,7 +19,7 @@ export const ErrorBanner = ({ actions, headingMessage, children }) => {
return (
<Alert variant="danger" icon={Info} actions={actionButtons}>
<Alert.Heading>
{headingMessage ? <FormattedMessage {...headingMessage} /> : null}
<FormattedMessage {...headingMessage} />
</Alert.Heading>
{children}
</Alert>

View File

@@ -9,77 +9,141 @@ 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,9 +1,10 @@
import React from 'react';
import { shallow } from 'enzyme';
import PDFRenderer from './PDFRenderer';
import { Document, Page } from 'react-pdf';
import { Form, IconButton } from '@edx/paragon';
import * as hooks from './pdfHooks';
import PDFRenderer from './PDFRenderer';
jest.mock('react-pdf', () => ({
pdfjs: { GlobalWorkerOptions: {} },
@@ -11,47 +12,210 @@ jest.mock('react-pdf', () => ({
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} />)).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} />)).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

@@ -3,21 +3,23 @@ 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} />)).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={
Object {
"height": 200,
"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={
Object {
"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

@@ -12,11 +12,7 @@ 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>}
>
<Collapsible className="file-collapsible" defaultOpen title={<h3 className="file-card-title">{file.name}</h3>}>
<div className="preview-panel">
<FileInfo><FilePopoverContent {...file} /></FileInfo>
{children}

View File

@@ -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

@@ -8,7 +8,6 @@ import {
} 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,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 '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} />)).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} />)).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

@@ -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={
Object {
"downloadUrl": "file download url",
"name": "filename.txt",
"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={
Object {
"downloadUrl": "file download url",
"name": "filename.txt",
"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 = {
headerMessage: 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.headerMessage).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.headerMessage).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 'enzyme';
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).toMatchSnapshot();
expect(el.find('title').text()).toContain(getConfig().SITE_NAME);
expect(el.find('link').prop('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>

View File

@@ -10,8 +10,6 @@ import {
import { InfoOutline } from '@edx/paragon/icons';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { nullMethod } from 'hooks';
import messages from './messages';
/**
@@ -23,7 +21,7 @@ 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>
)}
@@ -39,7 +37,7 @@ export const InfoPopover = ({ onClick, children, intl }) => (
);
InfoPopover.defaultProps = {
onClick: nullMethod,
onClick: () => {},
};
InfoPopover.propTypes = {
onClick: PropTypes.func,

View File

@@ -4,29 +4,21 @@ import PropTypes from 'prop-types';
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 'enzyme';
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).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).toMatchSnapshot();
});
test('`locked` shows light button variant and message', () => {
const el = render(gradingStatuses.locked);
expect(el).toMatchSnapshot();
});
test('`graded` shows success button variant and message', () => {
const el = render(gradingStatuses.graded);
expect(el).toMatchSnapshot();
});
test('`inProgress` shows warning button variant and message', () => {
const el = render(gradingStatuses.inProgress);
expect(el).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`] = `""`;
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 'enzyme';
import { CTA } from '.';
describe('CTA component', () => {
test('snapshots', () => {
const el = shallow(<CTA hide />);
expect(el).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 '@edx/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);

View File

@@ -0,0 +1,38 @@
import React from 'react';
import { shallow } from 'enzyme';
import { AppContext } from '@edx/frontend-platform/react';
import { Header } from '.';
jest.mock('./AnonymousUserMenu', () => 'AnonymousUserMenu');
jest.mock('./AuthenticatedUserDropdown', () => 'AuthenticatedUserDropdown');
jest.mock('./LinkedLogo', () => 'LinkedLogo');
jest.mock('./CourseLabel', () => 'CourseLabel');
jest.mock('@edx/frontend-platform/react', () => ({
AppContext: { authenticatedUser: null },
}));
jest.mock('react', () => ({
...jest.requireActual('react'),
useContext: (context) => context,
}));
const courseData = {
courseOrg: 'course-org',
courseNumber: 'course-number',
courseTitle: 'course-title',
};
describe('Header component', () => {
const props = {
...courseData,
intl: { formatMessage: (msg) => msg.defaultMessage },
};
test('snapshot', () => {
expect(shallow(<Header {...props} />)).toMatchSnapshot();
});
test('snapshot with authenticatedUser', () => {
AppContext.authenticatedUser = { username: 'test' };
expect(shallow(<Header {...props} />)).toMatchSnapshot();
});
});

View File

@@ -0,0 +1,56 @@
import { defineMessages } from '@edx/frontend-platform/i18n';
const messages = defineMessages({
courseMaterial: {
id: 'learn.navigation.course.tabs.label',
defaultMessage: 'Course Material',
description: 'The accessible label for course tabs navigation',
},
dashboard: {
id: 'header.menu.dashboard.label',
defaultMessage: 'Dashboard',
description: 'The text for the user menu Dashboard navigation link.',
},
help: {
id: 'header.help.label',
defaultMessage: 'Help',
description: 'The text for the link to the Help Center',
},
profile: {
id: 'header.menu.profile.label',
defaultMessage: 'Profile',
description: 'The text for the user menu Profile navigation link.',
},
account: {
id: 'header.menu.account.label',
defaultMessage: 'Account',
description: 'The text for the user menu Account navigation link.',
},
orderHistory: {
id: 'header.menu.orderHistory.label',
defaultMessage: 'Order History',
description: 'The text for the user menu Order History navigation link.',
},
skipNavLink: {
id: 'header.navigation.skipNavLink',
defaultMessage: 'Skip to main content.',
description: 'A link used by screen readers to allow users to skip to the main content of the page.',
},
signOut: {
id: 'header.menu.signOut.label',
defaultMessage: 'Sign Out',
description: 'The label for the user menu Sign Out action.',
},
registerSentenceCase: {
id: 'header.register.sentenceCase',
defaultMessage: 'Register',
description: 'Text in a button, prompting the user to register.',
},
signInSentenceCase: {
id: 'header.signIn.sentenceCase',
defaultMessage: 'Sign in',
description: 'Text in a button, prompting the user to log in.',
},
});
export default messages;

View File

@@ -163,7 +163,7 @@ describe('Criterion Feedback', () => {
});
describe('mapStateToProps', () => {
const testState = { arbitraryState: 'some data' };
const testState = { abitaryState: 'some data' };
const ownProps = { orderNum: props.orderNum };
let mapped;
beforeEach(() => {

View File

@@ -33,25 +33,27 @@ export class RadioCriterion extends React.Component {
isInvalid,
} = this.props;
return (
<Form.RadioSet name={config.name} value={data}>
{config.options.map((option) => (
<Form.Radio
className="criteria-option"
key={option.name}
value={option.name}
description={intl.formatMessage(messages.optionPoints, { points: option.points })}
onChange={this.onChange}
disabled={!isGrading}
>
{option.label}
</Form.Radio>
))}
{isInvalid && (
<Form.Control.Feedback type="invalid" className="feedback-error-msg">
{intl.formatMessage(messages.rubricSelectedError)}
</Form.Control.Feedback>
)}
</Form.RadioSet>
<>
<Form.RadioSet name={config.name} value={data}>
{config.options.map((option) => (
<Form.Radio
className="criteria-option"
key={option.name}
value={option.name}
description={intl.formatMessage(messages.optionPoints, { points: option.points })}
onChange={this.onChange}
disabled={!isGrading}
>
{option.label}
</Form.Radio>
))}
{isInvalid && (
<Form.Control.Feedback type="invalid" className="feedback-error-msg">
{intl.formatMessage(messages.rubricSelectedError)}
</Form.Control.Feedback>
)}
</Form.RadioSet>
</>
);
}
}

View File

@@ -40,14 +40,14 @@ describe('Radio Criterion Container', () => {
feedback: 'feedback mock',
options: [
{
explanation: 'explanation',
explanation: 'explaination',
feedback: 'option feedback',
label: 'this label',
name: 'option name',
points: 1,
},
{
explanation: 'explanation 2',
explanation: 'explaination 2',
feedback: 'option feedback 2',
label: 'this label 2',
name: 'option name 2',
@@ -129,7 +129,7 @@ describe('Radio Criterion Container', () => {
});
describe('mapStateToProps', () => {
const testState = { arbitrary: 'some data' };
const testState = { arbitary: 'some data' };
const ownProps = { orderNum: props.orderNum };
let mapped;
beforeEach(() => {

View File

@@ -29,14 +29,14 @@ describe('Review Crition Container', () => {
feedback: 'feedback mock',
options: [
{
explanation: 'explanation',
explanation: 'explaination',
feedback: 'option feedback',
label: 'this label',
name: 'option name',
points: 1,
},
{
explanation: 'explanation 2',
explanation: 'explaination 2',
feedback: 'option feedback 2',
label: 'this label 2',
name: 'option name 2',
@@ -78,7 +78,7 @@ describe('Review Crition Container', () => {
});
describe('mapStateToProps', () => {
const testState = { arbitrary: 'some data' };
const testState = { arbitary: 'some data' };
const ownProps = { orderNum: props.orderNum };
let mapped;
beforeEach(() => {

View File

@@ -1,85 +1,91 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Radio Criterion Container snapshot is grading 1`] = `
<Form.RadioSet
name="random name"
value="selected radio option"
>
<Form.Radio
className="criteria-option"
description="1 points"
disabled={false}
onChange={[MockFunction this.onChange]}
value="option name"
<React.Fragment>
<Form.RadioSet
name="random name"
value="selected radio option"
>
this label
</Form.Radio>
<Form.Radio
className="criteria-option"
description="2 points"
disabled={false}
onChange={[MockFunction this.onChange]}
value="option name 2"
>
this label 2
</Form.Radio>
</Form.RadioSet>
<Form.Radio
className="criteria-option"
description="1 points"
disabled={false}
onChange={[MockFunction this.onChange]}
value="option name"
>
this label
</Form.Radio>
<Form.Radio
className="criteria-option"
description="2 points"
disabled={false}
onChange={[MockFunction this.onChange]}
value="option name 2"
>
this label 2
</Form.Radio>
</Form.RadioSet>
</React.Fragment>
`;
exports[`Radio Criterion Container snapshot is not grading 1`] = `
<Form.RadioSet
name="random name"
value="selected radio option"
>
<Form.Radio
className="criteria-option"
description="1 points"
disabled={true}
onChange={[MockFunction this.onChange]}
value="option name"
<React.Fragment>
<Form.RadioSet
name="random name"
value="selected radio option"
>
this label
</Form.Radio>
<Form.Radio
className="criteria-option"
description="2 points"
disabled={true}
onChange={[MockFunction this.onChange]}
value="option name 2"
>
this label 2
</Form.Radio>
</Form.RadioSet>
<Form.Radio
className="criteria-option"
description="1 points"
disabled={true}
onChange={[MockFunction this.onChange]}
value="option name"
>
this label
</Form.Radio>
<Form.Radio
className="criteria-option"
description="2 points"
disabled={true}
onChange={[MockFunction this.onChange]}
value="option name 2"
>
this label 2
</Form.Radio>
</Form.RadioSet>
</React.Fragment>
`;
exports[`Radio Criterion Container snapshot radio contain invalid response 1`] = `
<Form.RadioSet
name="random name"
value="selected radio option"
>
<Form.Radio
className="criteria-option"
description="1 points"
disabled={false}
onChange={[MockFunction this.onChange]}
value="option name"
<React.Fragment>
<Form.RadioSet
name="random name"
value="selected radio option"
>
this label
</Form.Radio>
<Form.Radio
className="criteria-option"
description="2 points"
disabled={false}
onChange={[MockFunction this.onChange]}
value="option name 2"
>
this label 2
</Form.Radio>
<Form.Control.Feedback
className="feedback-error-msg"
type="invalid"
>
Rubric selection is required
</Form.Control.Feedback>
</Form.RadioSet>
<Form.Radio
className="criteria-option"
description="1 points"
disabled={false}
onChange={[MockFunction this.onChange]}
value="option name"
>
this label
</Form.Radio>
<Form.Radio
className="criteria-option"
description="2 points"
disabled={false}
onChange={[MockFunction this.onChange]}
value="option name 2"
>
this label 2
</Form.Radio>
<Form.Control.Feedback
className="feedback-error-msg"
type="invalid"
>
Rubric selection is required
</Form.Control.Feedback>
</Form.RadioSet>
</React.Fragment>
`;

View File

@@ -19,7 +19,7 @@ exports[`Criterion Container snapshot is graded and is not grading 1`] = `
this label
</strong>
<br />
explanation
explaination
</div>
<div
className="help-popover-option"
@@ -29,7 +29,7 @@ exports[`Criterion Container snapshot is graded and is not grading 1`] = `
this label 2
</strong>
<br />
explanation 2
explaination 2
</div>
</InfoPopover>
</Form.Label>
@@ -67,7 +67,7 @@ exports[`Criterion Container snapshot is ungraded and is grading 1`] = `
this label
</strong>
<br />
explanation
explaination
</div>
<div
className="help-popover-option"
@@ -77,7 +77,7 @@ exports[`Criterion Container snapshot is ungraded and is grading 1`] = `
this label 2
</strong>
<br />
explanation 2
explaination 2
</div>
</InfoPopover>
</Form.Label>
@@ -115,7 +115,7 @@ exports[`Criterion Container snapshot is ungraded and is not grading 1`] = `
this label
</strong>
<br />
explanation
explaination
</div>
<div
className="help-popover-option"
@@ -125,7 +125,7 @@ exports[`Criterion Container snapshot is ungraded and is not grading 1`] = `
this label 2
</strong>
<br />
explanation 2
explaination 2
</div>
</InfoPopover>
</Form.Label>

View File

@@ -34,14 +34,14 @@ describe('Criterion Container', () => {
feedback: 'feedback mock',
options: [
{
explanation: 'explanation',
explanation: 'explaination',
feedback: 'option feedback',
label: 'this label',
name: 'option name',
points: 2,
},
{
explanation: 'explanation 2',
explanation: 'explaination 2',
feedback: 'option feedback 2',
label: 'this label 2',
name: 'option name 2',
@@ -89,16 +89,16 @@ describe('Criterion Container', () => {
});
test('is ungraded and is grading (Radio criterion get render)', () => {
const rubricCriteria = el.find('.rubric-criteria');
expect(rubricCriteria.children(0).name()).toEqual('RadioCriterion');
const rubricCritera = el.find('.rubric-criteria');
expect(rubricCritera.children(0).name()).toEqual('RadioCriterion');
});
test('is ungraded and is not grading (Review criterion get render)', () => {
el.setProps({
isGrading: false,
});
const rubricCriteria = el.find('.rubric-criteria');
expect(rubricCriteria.children(0).name()).toEqual('ReviewCriterion');
const rubricCritera = el.find('.rubric-criteria');
expect(rubricCritera.children(0).name()).toEqual('ReviewCriterion');
});
test('is graded and is not grading (Radio criterion get render)', () => {
@@ -106,13 +106,13 @@ describe('Criterion Container', () => {
isGrading: false,
gradeStatus: gradeStatuses.graded,
});
const rubricCriteria = el.find('.rubric-criteria');
expect(rubricCriteria.children(0).name()).toEqual('RadioCriterion');
const rubricCritera = el.find('.rubric-criteria');
expect(rubricCritera.children(0).name()).toEqual('RadioCriterion');
});
});
describe('mapStateToProps', () => {
const testState = { arbitraryState: 'some data' };
const testState = { abitaryState: 'some data' };
const ownProps = { orderNum: props.orderNum };
let mapped;
beforeEach(() => {

View File

@@ -14,7 +14,7 @@ const messages = defineMessages({
optional: {
id: 'ora-grading.CriterionFeedback.optional',
defaultMessage: '(Optional)',
description: 'additional label for optional feedback field',
description: 'addtional label for optional feedback field',
},
optionPoints: {
id: 'ora-grading.RadioCriterion.optionPoints',

View File

@@ -16,7 +16,7 @@ exports[`DemoWarning component snapshots snapshot: disabled flag is present 1`]
</Alert.Heading>
<p>
<FormattedMessage
defaultMessage="You are demoing the new ORA staff grading experience. You will be unable to submit grades until you activate the feature. This will become the default grading experience on May 9th (05/09/2022). To opt-in early, or opt-out, please contact Partner Support."
defaultMessage="You are using the Demo Mode of the new Enhanced ORA Staff Grader interface. You will be unable to submit grades until you activate the feature."
description="Demo mode message"
id="ora-grading.ReviewModal.demoMessage"
/>

View File

@@ -10,7 +10,7 @@ const messages = defineMessages({
},
demoModeMessage: {
id: 'ora-grading.ReviewModal.demoMessage',
defaultMessage: 'You are demoing the new ORA staff grading experience. You will be unable to submit grades until you activate the feature. This will become the default grading experience on May 9th (05/09/2022). To opt-in early, or opt-out, please contact Partner Support.',
defaultMessage: 'You are using the Demo Mode of the new Enhanced ORA Staff Grader interface. You will be unable to submit grades until you activate the feature.',
description: 'Demo mode message',
},
});

View File

@@ -9,7 +9,7 @@ export const filterHooks = () => {
if (!setAllFilters || !state.filters) {
return {};
}
const clearFilters = React.useCallback(() => setAllFilters([]), [setAllFilters]);
const clearFilters = React.useCallback(() => setAllFilters([]), []);
const headerMap = headers.reduce(
(obj, cur) => ({ ...obj, [cur.id]: cur.Header }),
{},

View File

@@ -34,7 +34,7 @@ describe('FilterStatusComponent hooks', () => {
it('uses React.useCallback to clear filters, only once', () => {
mockTableContext(context);
const { cb, prereqs } = module.filterHooks().clearFilters.useCallback;
expect(prereqs).toEqual([context.setAllFilters]);
expect(prereqs).toEqual([]);
expect(context.setAllFilters).not.toHaveBeenCalled();
cb();
expect(context.setAllFilters).toHaveBeenCalledWith([]);

View File

@@ -19,10 +19,3 @@ span.pgn__icon.breadcrumb-arrow {
}
}
.submissions-table {
.pgn__data-table-filters-breakout-filter {
.pgn__form-group {
margin-bottom: 0;
}
}
}

View File

@@ -22,7 +22,7 @@ export const ListViewBreadcrumb = ({ courseId, oraName }) => (
</Hyperlink>
<p className="py-4">
<span className="h3">{oraName}</span>
<Hyperlink className="align-middle" destination={urls.ora(courseId, locationId())}>
<Hyperlink className="align-middle" destination={urls.ora(courseId, locationId)}>
<Icon src={Launch} className="d-inline-block" />
</Hyperlink>
</p>

View File

@@ -50,7 +50,7 @@ describe('ListViewBreadcrumb component', () => {
test('ora destination', () => {
expect(
el.find(Hyperlink).at(1).props().destination,
).toEqual(urls.ora(props.courseId, constants.locationId()));
).toEqual(urls.ora(props.courseId, constants.locationId));
});
});
describe('mapStateToProps', () => {

View File

@@ -1,27 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Button } from '@edx/paragon';
import { FormattedMessage } from '@edx/frontend-platform/i18n';
import messages from './messages';
export const SelectedBulkAction = ({ selectedFlatRows, handleClick }) => (
<Button
onClick={handleClick(selectedFlatRows)}
variant="primary"
className="view-selected-responses-btn"
>
<FormattedMessage {...messages.viewSelectedResponses} values={{ value: selectedFlatRows.length }} />
</Button>
);
SelectedBulkAction.defaultProps = {
selectedFlatRows: [],
};
SelectedBulkAction.propTypes = {
selectedFlatRows: PropTypes.arrayOf(PropTypes.object),
handleClick: PropTypes.func.isRequired,
};
export default SelectedBulkAction;

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