Compare commits

..

142 Commits

Author SHA1 Message Date
Syed Ali Abbas Zaidi
141518f890 build: bump frontend-build to alpha 2023-08-01 16:03:34 +05:00
leangseu-edx
78ada8ce34 chore: disable cycle dependency error for eslint (#227)
* chore: disable cycle dependency error for eslint

* chore: remove done from async function

* chore: downgrade @testing-library/react to 12 for react@16 support

* fix: update integration test selection and waiting

---------

Co-authored-by: Ben Warzeski <bwarzesk@gmail.com>
2023-07-17 11:17:58 -04:00
renovate[bot]
194e61380c fix(deps): update dependency @testing-library/user-event to v14 2023-07-16 09:58:12 -04:00
renovate[bot]
fa36e20de9 chore(deps): update dependency jest to v29 2023-07-16 05:34:46 -04:00
renovate[bot]
61cf386ee6 chore(deps): update dependency @testing-library/react to v14 2023-07-16 03:27:30 -04:00
renovate[bot]
445cd15d9a fix(deps): update react-router monorepo to v5.3.4 2023-07-16 00:32:05 -04:00
renovate[bot]
db1cf48257 fix(deps): update dependency redux-thunk to v2.4.2 2023-07-15 20:19:32 -04:00
renovate[bot]
942d471097 fix(deps): update dependency redux to v4.2.1 2023-07-15 17:34:16 -04:00
renovate[bot]
1a769a4e70 fix(deps): update dependency query-string to v7.1.3 2023-07-15 15:15:53 -04:00
renovate[bot]
b5cb2af513 fix(deps): update dependency prop-types to v15.8.1 2023-07-15 11:46:48 -04:00
renovate[bot]
6666c0df83 fix(deps): update dependency history to v5.3.0 2023-07-15 09:28:25 -04:00
renovate[bot]
fa60d7d234 fix(deps): update dependency dompurify to v2.4.7 2023-07-15 05:53:57 -04:00
renovate[bot]
78cce21f10 fix(deps): update dependency core-js to v3.31.1 2023-07-15 03:31:40 -04:00
renovate[bot]
e21c2a63e7 fix(deps): update dependency axios to ^0.27.0 2023-07-14 23:40:41 -04:00
renovate[bot]
514792786d fix(deps): update dependency @zip.js/zip.js to v2.7.20 2023-07-14 21:59:46 -04:00
renovate[bot]
5ef2f1ba4f fix(deps): update dependency @fortawesome/react-fontawesome to ^0.2.0 2023-07-14 17:25:11 -04:00
renovate[bot]
b986849c85 fix(deps): update dependency @edx/frontend-platform to v4.6.0 2023-07-14 15:16:53 -04:00
renovate[bot]
f8565c30d1 fix(deps): update dependency @edx/paragon to v20.45.1 2023-07-14 11:52:35 -04:00
renovate[bot]
3a7e103317 chore(deps): update dependency @edx/frontend-build to v12.8.65 2023-07-14 10:29:33 -04:00
renovate[bot]
f977e14ea6 fix(deps): update dependency @edx/frontend-component-header to v4.4.3 2023-07-14 05:47:28 -04:00
renovate[bot]
ab4f1864f2 fix(deps): update dependency @edx/frontend-component-header to v4.4.2 2023-07-14 03:10:45 -04:00
renovate[bot]
6e4d4c479c fix(deps): update dependency @edx/frontend-component-footer to v12.1.2 2023-07-14 00:33:53 -04:00
renovate[bot]
d74532f988 fix(deps): update dependency @edx/brand to v2.1.2 2023-07-13 21:26:17 -04:00
renovate[bot]
d577bc79f7 chore(deps): update dependency @edx/frontend-build to v12.8.64 2023-07-13 19:25:50 -04:00
renovate[bot]
43ab328545 chore(deps): update dependency node to 18.16 2023-07-13 15:48:22 -04:00
renovate[bot]
3ddfdf34d0 chore(deps): update dependency jest-expect-message to v1.1.3 2023-07-13 13:36:24 -04:00
renovate[bot]
b751d41caf chore(deps): update dependency jest to v27.5.1 2023-07-13 08:05:51 -04:00
renovate[bot]
4f4b28e6f5 chore(deps): update dependency @edx/reactifex to v2.2.0 2023-07-13 05:44:51 -04:00
renovate[bot]
038bd117e1 chore(deps): update dependency @edx/frontend-build to v12.8.63 2023-07-13 03:16:03 -04:00
renovate[bot]
7bb31a9aa0 fix(deps): update dependency util to v0.12.5 2023-07-13 00:03:44 -04:00
renovate[bot]
b2f59fc3a1 chore(deps): update dependency semantic-release to v19.0.5 2023-07-12 20:29:24 -04:00
renovate[bot]
60b63944bd chore(deps): update dependency enzyme-adapter-react-16 to v1.15.7 2023-07-12 18:50:22 -04:00
leangseu-edx
2893a9e698 chore: make renovate run weekly instead of daily 2023-07-12 14:57:38 -04:00
renovate[bot]
de4d0fb7f2 chore(deps): update dependency @testing-library/jest-dom to v5.16.5 2023-07-12 14:39:10 -04:00
leangseu-edx
ca7254c3b0 chore: update schedule to widen the range renovate to update 2023-07-12 14:37:33 -04:00
Leangseu Kim
6095869271 chore: change renovate to daily to for testing 2023-07-11 12:07:11 -04:00
leangseu-edx
8fe67f918f chore: remove depends on from catalog-info.yaml 2023-07-05 09:22:17 -04:00
leangseu-edx
b28e58e7cd chore: update links for catalog-info.yaml 2023-07-05 09:16:31 -04:00
leangseu-edx
a1436c3266 chore: update header and footer package (#195)
* chore: update header and footer package

* chore: unit test
2023-06-30 13:00:29 -04:00
Moisés Arévalo
9b5e85a236 chore: updated security contact email 2023-06-29 10:08:02 -04:00
Leangseu Kim
fe1388666a feat: add renovate for js dependency update 2023-06-28 11:31:54 -04:00
Leangseu Kim
a07d6f9b80 fix: when there is no options, the validation should be valid 2023-06-07 11:04:41 -04:00
Adolfo R. Brandes
3e685be116 Merge pull request #183 from arbrandes/runtime-config 2023-05-31 16:42:09 +01:00
Adolfo R. Brandes
0bb5f50917 refactor: use getConfig 2023-05-31 12:32:09 -03:00
Adolfo R. Brandes
49357a4e87 feat: Support runtime configuration
frontend-platform supports runtime configuration since 2.5.0 (see the PR
that introduced it[1], but it requires MFE cooperation.  This implements
just that: by avoiding making configuration values constant, it should
now be possible to change them after initialization.

Almost all changes here relate to the `LMS_BASE_URL` setting, which in
most places was treated as a constant.

[1] openedx/frontend-platform#335
2023-05-25 11:28:28 -03:00
Leangseu Kim
33ba1cdd08 feat: upgraded to node v18, added .nvmrc and updated workflows 2023-05-22 11:20:32 -04:00
Nathan Sprenkle
7012fa82c9 docs: fix bad readme styling (#182) 2023-05-15 11:08:48 -04:00
Adolfo R. Brandes
7b418ff6e3 Merge pull request #167 from raccoongang/fix-location-id 2023-05-15 10:37:00 -03:00
Eugene Dyudyunov
cc349faeb2 fix: BadOraLocationResponse error
Refactor the locationId constant for the subdirectory-based
deployments support.

Exclude the MFE's `PUBLIC_PATH` from the constant.

The `window.location.pathname` example:
```
<PUBLIC_PATH>block-v1:oragrading+oragrading+oragrading+type@openassessment+block@ee217e897a954c1faa3b29317da0f2e7
```
Where the `PUBLIC_PATH` could be:
- `'/'` - for subdomain-based deployments (default)
- `'/mfe-specifix-public-path/'` - for subdirectory-based deployments
2023-05-12 15:33:01 +03:00
Nathan Sprenkle
455ca15af9 docs: remove old readme.md (#180) 2023-05-11 13:36:32 -04:00
Nathan Sprenkle
f992331bf4 docs: maintainership prep (#178)
* docs: add codeowners file

* docs: add catalog-info.yaml

* docs: update README

* docs: add deploy information

* docs: update contributing info
2023-05-09 14:30:31 -04:00
Omar Al-Ithawi
4158231d7a feat: use atlas in make pull_translations (#179)
Changes
-------
 - Bump frontend-platform to bring `intl-imports.js` script
 - Move all i18n imports into `src/i18n/index.js` so `intl-imports.js` can
   override it with latest translations
 - Add `atlas` into `make pull_translations` when `OPENEDX_ATLAS_PULL`
   environment variable is set.
 - package.json and package-lock.json are copied from https://github.com/openedx/frontend-app-ora-grading/pull/176
 - updated snapshots and updated tests in sync with `frontend-platform`
 - require package-lock.json version 3: same as https://github.com/openedx/frontend-app-ora-grading/pull/176

Refs: [FC-0012 project](https://openedx.atlassian.net/l/cp/XGS0iCcQ) implementing Translation Infrastructure OEP-58.
2023-05-09 10:12:40 -04:00
Jenkins
2fa46ab00e chore(i18n): update translations 2023-04-16 11:45:43 -04:00
Jenkins
adade6e48d chore(i18n): update translations 2023-03-26 11:50:42 -04:00
Yoiber
06aea1ff68 chore(i18n): add more languages (#160) 2023-03-25 13:00:56 -04:00
Mashal Malik
054304902f refactor: remove unused tranisfex v2 url (#172) 2023-03-06 12:18:24 +05:00
Leangseu Kim
ba9bddbda1 fix: removed coveralls and codecov packages with update in ci uploader 2023-03-02 09:51:13 -05:00
Feanil Patel
706d69aeca build: Creating a missing workflow file add-remove-label-on-comment.yml.
The .github/workflows/add-remove-label-on-comment.yml workflow is missing or needs an update to stay in
sync with the current standard for this workflow as defined in the
`.github` repo of the `openedx` GitHub org.
2023-02-28 10:32:02 -05:00
Feanil Patel
6d3ed03cac build: Creating a missing workflow file add-depr-ticket-to-depr-board.yml.
The .github/workflows/add-depr-ticket-to-depr-board.yml workflow is missing or needs an update to stay in
sync with the current standard for this workflow as defined in the
`.github` repo of the `openedx` GitHub org.
2023-02-28 10:32:02 -05:00
Feanil Patel
21a35cde82 build: Creating a missing workflow file self-assign-issue.yml.
The .github/workflows/self-assign-issue.yml workflow is missing or needs an update to stay in
sync with the current standard for this workflow as defined in the
`.github` repo of the `openedx` GitHub org.
2023-02-28 10:32:02 -05:00
Leangseu Kim
66f85ee17e chore: upgrade transifex push translation to v3 2023-02-23 10:54:53 -05:00
Leangseu Kim
140cfc1639 chore: upgrade transifex push translation to v3 2023-02-23 10:30:35 -05:00
Leangseu Kim
26906d45f7 fix: upgrade frontend-build to v12 2023-02-23 10:20:59 -05:00
Leangseu Kim
a753170cb7 chore!: Dropped support for Node 12 2023-02-23 09:56:25 -05:00
Jenkins
690140ce46 chore(i18n): update translations 2023-02-22 10:44:03 -05:00
Nathan Sprenkle
6764a9766c docs: add CODEOWNERS (#149) 2022-12-06 16:38:39 -05:00
Diana Olarte
c646b88543 feat: allow runtime configuration (#144)
* feat: allow runtime configuration

* test: organize Head test
2022-11-21 10:06:24 -05:00
Leangseu Kim
b1d11119db fix: update transifex flag for tx cli 1.4.0 2022-10-18 12:09:17 -04:00
edX requirements bot
35532fed92 fix: update organization references (#142) 2022-10-03 12:56:21 +05:00
Nathan Sprenkle
15952d808a Merge pull request #141 from edx/transifex-bot-update-translations2022-09-18
chore(i18n): update translations
2022-09-19 15:30:58 -04:00
Jenkins
3a928e42bc chore(i18n): update translations 2022-09-18 15:40:16 +00:00
Ben Warzeski
15e756673f fix: remove return from useEffect call (#131)
* fix: remove return from useEffect call

* fix: text renderer tests
2022-07-20 13:29:26 -04:00
Leangseu Kim
cba03d305c chore: update rubric style 2022-07-19 13:32:22 -04:00
Leangseu Kim
956dee9a6d chore: change card body to card section 2022-07-19 11:28:15 -04:00
leangseu-edx
4f7d3aeb57 leangseu edx/header footer dependency (#127)
* chore: update dependency

* fix: update dependency to match deploy package for header and footer

* chore: update linting

* chore: update datatable filter for paragon upgrade
2022-07-15 11:02:44 -04:00
leangseu-edx
d4f1383822 fix: make student response persist break line on display (#125)
* fix: make student response persist break line on display

* chore: scroll bug when selecting text
2022-07-07 11:54:47 -04:00
Nathan Sprenkle
5efd1466bf Merge pull request #121 from edx/nsprenkle/readme
docs: add a readme
2022-06-21 14:00:23 -04:00
nsprenkle
36bd27517c docs: update reamde 2022-06-17 11:14:55 -04:00
nsprenkle
6c884ce215 docs: add a basic readme 2022-06-17 11:09:55 -04:00
Leangseu Kim
8b4f554cf6 fix: use moment to handle date 2022-06-13 15:08:26 -04:00
Leangseu Kim
0b1b079abd fix: patch to name duplicate 2022-06-13 10:14:23 -04:00
Leangseu Kim
b2c52111d7 chore: update text on CTA banner 2022-05-20 12:51:46 -04:00
leangseu-edx
18bc94e2ff chore: add CTA for page (#112)
* chore: add CTA for page

* chore: update hyperlink style
2022-05-19 10:16:26 -04:00
leangseu-edx
0f41df2cf3 feat: add fetch submission files (#110)
chore: remove cache busting
2022-05-12 09:45:46 -04:00
Ben Warzeski
91fbb8978a chore: update integration tests (#109) 2022-05-11 14:14:25 -04:00
leangseu-edx
5aecd88c70 fix: loose end on hook refactor (#111)
* fix: loose end on hook refactor

* chore: update package-lock.json to npm 8
2022-05-11 13:05:01 -04:00
Jawayria
2bf499fb43 Merge pull request #107 from edx/jenkins/version-check-0a90024
feat: Add package-lock file version check
2022-05-06 15:59:20 +05:00
edx-semantic-release
c217c32196 chore(i18n): update translations 2022-05-01 11:45:23 -04:00
Ben Warzeski
5f12c4fb8e chore: renderer test coverage (#103)
* chore: renderer test coverage

* fix: lint

* chore: api tests

* chore: tests for app reducer and StartGradeButton

* chore: lint

* fix: update reducer tests

* chore: more test coverage

* chore: test coverage

* chore: update test for merge conflicts
2022-04-29 14:54:33 -04:00
edX requirements bot
4d7d95e490 feat: Add package-lock file version check 2022-04-29 08:51:09 -04:00
Matthew Carter
0a90024de9 feat: Update Demo Mode banner (#105)
* chore: Update MFE page title

* feat: Demo mode banner includes end date and call to action
2022-04-27 12:55:10 -04:00
edx-semantic-release
91d06e9788 chore(i18n): update translations 2022-04-24 11:45:23 -04:00
Leangseu Kim
74423bf359 feat: prevent download large files 2022-04-21 09:37:11 -04:00
leangseu-edx
7e9eab24b0 header component (#101)
* chore: use LearningHeader instead course header

* chore: remove course header debris
2022-04-20 13:13:03 -04:00
leangseu-edx
91dd10917f fix: cannot select criterion (#100)
* fix: cannot select criterion

* fix: refactor fill grade data

* fix: update tests

Co-authored-by: Ben Warzeski <bwarzeski@edx.org>
2022-04-18 16:43:37 -04:00
edx-semantic-release
b2098be114 chore(i18n): update translations 2022-04-17 11:50:11 -04:00
leangseu-edx
64ac98c310 download filename, error handling and cache busting (#98)
* feat: handle download error and display them

* chore: update test environment for easier single file test

* feat: add cache bursting to the download
2022-04-14 13:52:39 -04:00
Leangseu Kim
8a80e2a70e chore: update package 2022-04-12 10:36:25 -04:00
Matthew Carter
a936d970db Merge pull request #95 from muselesscreator/batch_unlock_api
feat: Batch unlock api
2022-04-11 10:56:21 -04:00
edx-semantic-release
c7d1cdecff chore(i18n): update translations 2022-04-10 11:49:58 -04:00
Ben Warzeski
56c6c88638 feat: connect batch unlock to the api 2022-04-07 15:55:49 -04:00
Ben Warzeski
9c42bfbd8a fix: update snapshot 2022-04-07 15:53:09 -04:00
Ben Warzeski
69733f7837 fix: update routing for images 2022-04-07 15:52:50 -04:00
edX requirements bot
9c564c5781 build: Added support for node v16 (#54)
* feat: Added support for node v16

* test: update test for download

Co-authored-by: Leangseu Kim <lkim@edx.org>
2022-04-06 14:13:25 -04:00
Ben Warzeski
6c9673cc06 feat: Demo flag (#89)
* fix: preserve gradingData on navigation

* feat: demo linking and clear-all on close review

* fix: fix assets url

* chore: update snapshot

* fix: update routing for images

* fix: update snapshot
2022-04-05 15:11:20 -04:00
edx-semantic-release
ea1b1c8673 chore(i18n): update translations 2022-04-04 09:02:19 -04:00
Usama Sadiq
62a6c7b282 Merge pull request #91 from edx/usamasadiq/update-pull-translations-command
build: updated transifex pull translations command
2022-04-04 16:39:31 +05:00
UsamaSadiq
803dd2f012 build: updated transifex pull translations command 2022-04-04 15:12:34 +05:00
Usama Sadiq
bf677db59a Merge pull request #88 from edx/jenkins/transifex-client-migration-876f5d9
fix: transifex migration to new client
2022-04-04 14:11:44 +05:00
Leangseu Kim
d033fa310d feat: make zip support larger files
chore: improve code readability
2022-03-30 13:12:56 -04:00
edX requirements bot
f6432c1896 fix: transifex migration to new client 2022-03-30 12:13:50 -04:00
Leangseu Kim
876f5d9413 fix: make gradeData check for empty object and null/undefined
chore: remove duplicate/unused function
2022-03-29 12:05:09 -04:00
Ben Warzeski
587caff980 Demo (#86)
* feat: demo mode

* chore: update i18n for demo mode

* chore: re-use demo design

* fix: update snapshots

* chore: add more testing of demo feature
2022-03-29 12:02:39 -04:00
Ben Warzeski
38d40342db feat: demo mode (#78)
* feat: demo mode

* chore: update i18n for demo mode

* chore: re-use demo design

* fix: update snapshots
2022-03-29 11:56:05 -04:00
Leangseu Kim
1595f52ade chore: update brand config 2022-03-29 09:47:36 -04:00
Matt Hughes
8d4ef50039 Merge pull request #81 from edx/transifex-bot-update-translations2022-03-20
chore(i18n): update translations
2022-03-28 13:23:06 -04:00
Ben Warzeski
257849b55f Chore: Au 340 (#82)
* chore: testing update

* fix: uncomment tests
2022-03-23 13:34:20 -04:00
edX Transifex Bot
e3d1dde3d1 chore(i18n): update translations 2022-03-20 15:40:15 +00:00
leangseu-edx
0de9807c94 Revert "Chore: Au 340 (#79)"
This reverts commit e1442f7890.
2022-03-18 12:24:56 -04:00
Ben Warzeski
e1442f7890 Chore: Au 340 (#79)
* chore: update tests for grading selectors

Co-authored-by: Justin Lapierre <jlapierre@edx.org>
2022-03-18 09:29:22 -04:00
Ben Warzeski
8ebd0497cf chore: update filter status component tests (#74)
* chore: update filter status component tests

* chore: move react stub to setupTests
2022-03-17 11:48:17 -04:00
Leangseu Kim
25aae2cec8 fix: fix bad export on filterStatusComponent 2022-03-17 07:13:53 -04:00
Leangseu Kim
e656534a51 feat: empty submission components
chore: update usage of empty submission in list view

chore: remove unused

test: add unit testing
2022-03-15 12:20:11 -04:00
Leangseu Kim
278ac101a7 feat: override filter status component
fix: review modal test failure

chore: update unit test

chore: update hook and test

chore: add unit test for set all filter

chore: update use callback mock
2022-03-15 12:04:20 -04:00
leangseu-edx
59c57028a0 chore: remove reloadSubmissions key from request and use initialize to reloadSubmissions table (#65)
Co-authored-by: Justin Lapierre <jlapierre@edx.org>
2022-02-23 09:25:34 -05:00
leangseu-edx
e549cfb598 feat: confirm close modal for unsaved work (#66)
* feat: confirm close modal for unsaved work

* chore: update prop
2022-02-23 09:23:36 -05:00
Leangseu Kim
4267206d58 fix: make feedback a text area 2022-02-15 11:39:32 -05:00
Leangseu Kim
054e6acfc6 fix: align icon link and ora header 2022-02-15 11:33:51 -05:00
leangseu-edx
664368fe17 fix: add margin between button (#62)
* fix: add margin between button

* chore: bump margin one size up
2022-02-15 11:33:24 -05:00
Leangseu Kim
6b94ba4c5c chore(i18n): update translations 2022-02-11 11:25:29 -05:00
leangseu-edx
11a2838799 chore: update translation config (#53)
* chore: update translation config

* fix: fix duplicate id

* chore: update snapshot
2022-02-10 11:49:24 -05:00
Ben Warzeski
42535c5f80 feat: switch over to real bff api (#56) 2022-02-10 11:25:44 -05:00
leangseu-edx
e8550af85d fix: scroll enable on smaller screen (#51)
* fix: scroll enable on smaller screen

* fix: fixed review error width

* fix: make rubric and review error width align

* fix: rubric position to be sticky

* fix: remove double scroll bar at the bottom for small screen

* fix: make submission more response on small screen

* fix: rubric sticky and margin correctly on smaller screen

* chore: linting and update snapshot

* fix: make submission title resize to smaller on smaller screen

* fix: miss align rubic and submission file on the top

* fix: padding size on small screen

* feat: add optional on feedback comment for clarity

* chore: update test
2022-02-08 10:33:42 -05:00
Leangseu Kim
c6ed6938af fix: update setup test to mock locationId 2022-02-02 15:09:14 -05:00
leangseu-edx
81026ae013 feat: make the app reload submission on modal close (#50)
* feat: make the app reload submission on modal close

* chore: remove reloadSubmission request and reuse initial app
2022-01-25 14:00:51 -05:00
leangseu-edx
46ebc899ee tri-fix (#49)
* fix: response display to show medium size on even the content isn't big enough

* fix: don't show download button when there isn't submission files

* feat: additional default support file for image renderer
2022-01-24 14:30:07 -05:00
leangseu-edx
f7341efc71 fix: grade data is empty object when there isn't grade data (#48)
* fix: grade data is empty object when there isn't grade data

* test: update unit testing
2022-01-21 13:36:54 -05:00
leangseu-edx
c5bf0a7d11 feat: add file renderer that support common error handling and loading banner (#42)
* feat: add file renderer that support common error handling and loading banner

* chore: use axios instead of get from utils

* chore: fixed typo and update snapshots
2022-01-19 12:34:44 -05:00
leangseu-edx
b2aac6036e feat: show file size in file popover (#47)
* feat: show file size in file popover

* chore: update file popover content's props
2022-01-19 11:13:39 -05:00
Ben Warzeski
21dcc972ed chore: update integration tests through grade submission (#43) 2022-01-13 14:18:58 -05:00
Justin Lapierre
d629e4495b chore: Implement submissions selector unit tests (#46)
* chore: Adapted the selector unit test to exercise the submissions selectors

* chore: updated linting for new submissions selector unit tests
2022-01-11 15:46:39 -05:00
Ben Warzeski
579ff50c99 feat: Zip download (#41)
* integration tests through navigation

* more tests

* download-demo

* feat: file download and tests

* fix: update manifest styling
2022-01-10 10:29:13 -05:00
286 changed files with 39972 additions and 28230 deletions

2
.env
View File

@@ -30,3 +30,5 @@ 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,3 +36,5 @@ 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,8 +4,14 @@ 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 Normal file
View File

@@ -0,0 +1,6 @@
# 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

@@ -0,0 +1,19 @@
# 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

@@ -0,0 +1,20 @@
# 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

@@ -3,50 +3,54 @@ name: Node CI
on:
push:
branches:
- master
- master
pull_request:
branches:
- "**"
- '**'
jobs:
build:
tests:
runs-on: ubuntu-20.04
steps:
- name: Checkout
uses: actions/checkout@v2
- name: Checkout
uses: actions/checkout@v3
- name: Setup Nodejs Env
run: echo "NODE_VER=`cat .nvmrc`" >> $GITHUB_ENV
- 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
with:
node-version: 18.15
- name: Setup Nodejs
uses: actions/setup-node@v2
with:
node-version: 12
- name: Install dependencies
run: npm ci
- name: Install dependencies
run: npm ci
- name: Verify No Uncommitted Changes
run: make validate-no-uncommitted-package-lock-changes
- name: Verify No Uncommitted Changes
run: make validate-no-uncommitted-package-lock-changes
- name: Lint
run: npm run lint
- name: Lint
run: npm run lint
- name: Test
run: npm run test
- name: Test
run: npm run test
- name: Build
run: npm run build
- name: Build
run: npm run build
- name: Run Coverage
uses: codecov/codecov-action@v3
- name: Run Coverage
uses: codecov/codecov-action@v2
- name: Send failure notification
if: ${{ failure() }}
uses: dawidd6/action-send-mail@v3
with:
server_address: email-smtp.us-east-1.amazonaws.com
server_port: 465
username: ${{ secrets.EDX_SMTP_USERNAME }}
password: ${{ secrets.EDX_SMTP_PASSWORD }}
subject: Upgrade python requirements workflow failed in ${{github.repository}}
to: masters-grades@edx.org
from: github-actions <github-actions@edx.org>
body: Upgrade python requirements workflow in ${{github.repository}} failed! For details see "github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}"
- name: Send failure notification
if: ${{ failure() }}
uses: dawidd6/action-send-mail@v3
with:
server_address: email-smtp.us-east-1.amazonaws.com
server_port: 465
username: ${{ secrets.EDX_SMTP_USERNAME }}
password: ${{ secrets.EDX_SMTP_PASSWORD }}
subject: Upgrade python requirements workflow failed in ${{github.repository}}
to: masters-grades@edx.org
from: github-actions <github-actions@edx.org>
body: Upgrade python requirements workflow in ${{github.repository}} failed!
For details see "github.com/${{ github.repository }}/actions/runs/${{ github.run_id
}}"

View File

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

View File

@@ -0,0 +1,13 @@
#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

12
.github/workflows/self-assign-issue.yml vendored Normal file
View File

@@ -0,0 +1,12 @@
# 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

3
.gitignore vendored
View File

@@ -19,6 +19,9 @@ public/samples/
.idea
.vscode
# Local package dependencies
module.config.js
### transifex ###
src/i18n/transifex_input.json
temp

1
.nvmrc Normal file
View File

@@ -0,0 +1 @@
18.16

View File

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

View File

@@ -2,19 +2,18 @@ npm-install-%: ## install specified % npm package
npm install $* --save-dev
git add package.json
transifex_resource = ora-enhanced-staff-grader
transifex_langs = "ar,fr,es_419,zh_CN"
transifex_resource = frontend-app-ora-grading
transifex_langs = "ar,fr,es_419,zh_CN,fr_CA,it_IT,pt_PT,de_DE,uk,ru,hi"
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 is-es5
NPM_TESTS=build i18n_extract lint test
.PHONY: test
test: $(addprefix test.npm.,$(NPM_TESTS)) ## validate ci suite
@@ -49,15 +48,30 @@ push_translations:
# Pushing strings to Transifex...
tx push -s
# Fetching hashes from Transifex...
./node_modules/reactifex/bash_scripts/get_hashed_strings.sh $(tx_url1)
./node_modules/@edx/reactifex/bash_scripts/get_hashed_strings_v3.sh
# Writing out comments to file...
$(transifex_utils) $(transifex_temp) --comments
$(transifex_utils) $(transifex_temp) --comments --v3-scripts-path
# Pushing comments to Transifex...
./node_modules/reactifex/bash_scripts/put_comments.sh $(tx_url2)
./node_modules/@edx/reactifex/bash_scripts/put_comments_v3.sh
ifeq ($(OPENEDX_ATLAS_PULL),)
# Pulls translations from Transifex.
pull_translations:
tx pull -f --mode reviewed --language=$(transifex_langs)
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
# This target is used by CI.
validate-no-uncommitted-package-lock-changes:

202
README.rst Normal file
View File

@@ -0,0 +1,202 @@
frontend-app-ora-grading
########################
|ci-badge| |codecov-badge| |doc-badge| |license-badge| |status-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
***************
Developing
==========
One Time Setup
--------------
First, clone the repo, install code prerequisites, and start the app.
.. code-block::
# Clone the repository
git clone git@github.com:openedx/frontend-app-ora-grading.git
cd frontend-app-ora-grading
# Install prerequisites
npm install
# Start the MFE
npm run 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
Getting Help
************
Documentation
=============
See `ORA Staff Grading on ReadTheDocs`_ for more in-depth usage information.
.. _ORA Staff Grading on ReadTheDocs: https://edx.readthedocs.io/projects/edx-partner-course-staff/en/latest/exercises_tools/open_response_assessments/ORA_Staff_Grading.html#ora-staff-grading
More 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`_.
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/
.. _Getting Help: https://openedx.org/getting-help
License
*******
The code in this repository is licensed under the GNU Affero General Public
License v3.0 unless otherwise noted.
Please see `LICENSE <LICENSE>`_ for details.
Contributing
************
This project is currently only accepting bug fixes, security fixes, and
maintenance work. New features should be discussed in advance with the owning
team and added to the public roadmap or they may not be accepted. You can start
a conversation by creating a new issue on this repo summarizing your feature
idea.
Please read `How To Contribute <https://openedx.org/r/how-to-contribute>`_ for
details.
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. Please email security@openedx.org.
.. |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: CI
.. |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
.. |doc-badge| image:: https://readthedocs.org/projects/frontend-app-ora-grading/badge/?version=latest
:target: https://edx.readthedocs.io/projects/edx-partner-course-staff/en/latest/exercises_tools/open_response_assessments/ORA_Staff_Grading.html
:alt: Documentation
.. |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

View File

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

19
catalog-info.yaml Normal file
View File

@@ -0,0 +1,19 @@
# 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

@@ -2,6 +2,7 @@ const { createConfig } = require('@edx/frontend-build');
module.exports = createConfig('jest', {
setupFilesAfterEnv: [
'jest-expect-message',
'<rootDir>/src/setupTest.js',
],
modulePaths: ['<rootDir>/src/'],
@@ -11,5 +12,10 @@ 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',
});

53029
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -8,8 +8,6 @@
},
"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/",
@@ -27,64 +25,70 @@
},
"dependencies": {
"@edx/brand": "npm:@edx/brand-edx.org@^2.0.3",
"@edx/frontend-component-footer": "10.1.6",
"@edx/frontend-platform": "1.12.4",
"@edx/paragon": "16.14.4",
"@edx/frontend-component-footer": "^12.0.0",
"@edx/frontend-component-header": "^4.2.2",
"@edx/frontend-platform": "^4.6.1",
"@edx/paragon": "^20.30.0",
"@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.1.15",
"@fortawesome/react-fontawesome": "^0.2.0",
"@redux-beacon/segment": "^1.1.0",
"@reduxjs/toolkit": "^1.6.1",
"@testing-library/user-event": "^13.5.0",
"@testing-library/user-event": "^14.0.0",
"@zip.js/zip.js": "^2.4.6",
"axios": "^0.27.0",
"classnames": "^2.3.1",
"core-js": "3.16.2",
"core-js": "3.31.1",
"dompurify": "^2.3.1",
"email-prop-type": "^3.0.1",
"enzyme": "^3.11.0",
"enzyme-to-json": "^3.6.2",
"file-saver": "^2.0.5",
"filesize": "^8.0.6",
"font-awesome": "4.7.0",
"history": "5.0.1",
"history": "5.3.0",
"html-react-parser": "^1.3.0",
"lodash": "^4.17.21",
"node-sass": "^6.0.1",
"prop-types": "15.7.2",
"query-string": "7.0.1",
"react": "17.0.2",
"react-dom": "17.0.2",
"moment": "^2.29.3",
"prop-types": "15.8.1",
"query-string": "7.1.3",
"react": "^16.14.0",
"react-dom": "^16.14.0",
"react-helmet": "^6.1.0",
"react-intl": "^5.20.9",
"react-pdf": "^5.5.0",
"react-redux": "^7.2.4",
"react-router": "5.2.0",
"react-router-dom": "5.2.0",
"react-router": "5.3.4",
"react-router-dom": "5.3.4",
"react-router-redux": "^5.0.0-alpha.9",
"redux": "4.1.1",
"redux": "4.2.1",
"redux-beacon": "^2.1.0",
"redux-devtools-extension": "2.13.9",
"redux-logger": "3.0.6",
"redux-thunk": "2.3.0",
"redux-thunk": "2.4.2",
"regenerator-runtime": "^0.13.9",
"reselect": "^4.0.0",
"util": "^0.12.4",
"whatwg-fetch": "^3.6.2"
},
"devDependencies": {
"@edx/frontend-build": "8.0.4",
"@edx/browserslist-config": "^1.2.0",
"@edx/frontend-build": "^12.9.0-alpha.1",
"@edx/reactifex": "^2.1.1",
"@testing-library/jest-dom": "^5.14.1",
"@testing-library/react": "^12.1.0",
"axios": "0.21.1",
"@testing-library/react": "^12.1.5",
"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": "27.0.6",
"react-dev-utils": "^11.0.4",
"react-test-renderer": "^17.0.2",
"jest": "29.6.1",
"jest-expect-message": "^1.0.2",
"react-dev-utils": "^12.0.1",
"react-test-renderer": "^16.14.0",
"reactifex": "1.1.1",
"redux-mock-store": "^1.5.4",
"semantic-release": "^17.4.5"
"semantic-release": "^19.0.3"
}
}

View File

@@ -1,7 +1,7 @@
<!doctype html>
<html lang="en-us" dir="ltr">
<head>
<title>ORA Enhanced Staff Grader | <%= process.env.SITE_NAME %></title>
<title>ORA staff grading | <%= 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" />

34
renovate.json Normal file
View File

@@ -0,0 +1,34 @@
{
"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,22 +4,28 @@ 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 ListView from 'containers/ListView';
import './App.scss';
import Head from './components/Head';
import Header from 'containers/CourseHeader';
export const App = ({ courseMetadata }) => (
export const App = ({ courseMetadata, isEnabled }) => (
<Router>
<div>
<Head />
<Header
courseTitle={courseMetadata.title}
courseNumber={courseMetadata.number}
courseOrg={courseMetadata.org}
/>
{!isEnabled && <DemoWarning />}
<CTA />
<main>
<ListView />
</main>
@@ -40,10 +46,12 @@ App.propTypes = {
number: PropTypes.string,
org: PropTypes.string,
}),
isEnabled: PropTypes.bool.isRequired,
};
export const mapStateToProps = (state) => ({
courseMetadata: selectors.app.courseMetadata(state),
isEnabled: selectors.app.isEnabled(state),
});
export default connect(mapStateToProps)(App);

View File

@@ -42,32 +42,6 @@ $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,14 +2,30 @@ 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';
import { App } from './App';
jest.mock('data/redux', () => ({
app: {
selectors: {
courseMetadata: (state) => ({ courseMetadata: state }),
isEnabled: (state) => ({ isEnabled: state }),
},
},
}));
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('containers/CourseHeader', () => 'CourseHeader');
jest.mock('components/Head', () => 'Head');
const logo = 'fakeLogo.png';
let el;
@@ -22,10 +38,14 @@ describe('App router component', () => {
number: 'course-number',
title: 'course-title',
},
isEnabled: true,
};
test('snapshot', () => {
test('snapshot: enabled', () => {
expect(shallow(<App {...props} />)).toMatchSnapshot();
});
test('snapshot: disabled (show demo warning)', () => {
expect(shallow(<App {...props} isEnabled={false} />)).toMatchSnapshot();
});
describe('component', () => {
beforeEach(() => {
process.env.LOGO_POWERED_BY_OPEN_EDX_URL_SVG = logo;
@@ -42,5 +62,16 @@ 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

@@ -1,13 +1,36 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`App router component snapshot 1`] = `
exports[`App router component snapshot: disabled (show demo warning) 1`] = `
<BrowserRouter>
<div>
<CourseHeader
<Head />
<Header
courseNumber="course-number"
courseOrg="course-org"
courseTitle="course-title"
/>
<DemoWarning />
<CTA />
<main>
<ListView />
</main>
<Footer
logo="https://edx-cdn.org/v3/stage/open-edx-tag.svg"
/>
</div>
</BrowserRouter>
`;
exports[`App router component snapshot: enabled 1`] = `
<BrowserRouter>
<div>
<Head />
<Header
courseNumber="course-number"
courseOrg="course-org"
courseTitle="course-title"
/>
<CTA />
<main>
<ListView />
</main>

View File

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

View File

@@ -0,0 +1,23 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`DemoAlert component snapshot 1`] = `
<AlertModal
footerNode={
<ActionRow>
<Button
onClick={[MockFunction props.onClose]}
variant="primary"
>
Confirm
</Button>
</ActionRow>
}
isOpen={true}
onClose={[MockFunction props.onClose]}
title="Demo submit prevented"
>
<p>
Grade submission is disabled in the Demo mode of the new ORA Staff Grader.
</p>
</AlertModal>
`;

View File

@@ -0,0 +1,39 @@
import React from 'react';
import PropTypes from 'prop-types';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import {
ActionRow,
AlertModal,
Button,
} from '@edx/paragon';
import messages from './messages';
export const DemoAlert = ({
intl: { formatMessage },
isOpen,
onClose,
}) => (
<AlertModal
title={formatMessage(messages.title)}
isOpen={isOpen}
onClose={onClose}
footerNode={(
<ActionRow>
<Button variant="primary" onClick={onClose}>
{formatMessage(messages.confirm)}
</Button>
</ActionRow>
)}
>
<p>{formatMessage(messages.warningMessage)}</p>
</AlertModal>
);
DemoAlert.propTypes = {
intl: intlShape.isRequired,
isOpen: PropTypes.bool.isRequired,
onClose: PropTypes.func.isRequired,
};
export default injectIntl(DemoAlert);

View File

@@ -0,0 +1,16 @@
import React from 'react';
import { shallow } from 'enzyme';
import { formatMessage } from 'testUtils';
import { DemoAlert } from '.';
describe('DemoAlert component', () => {
test('snapshot', () => {
const props = {
intl: { formatMessage },
isOpen: true,
onClose: jest.fn().mockName('props.onClose'),
};
expect(shallow(<DemoAlert {...props} />)).toMatchSnapshot();
});
});

View File

@@ -0,0 +1,21 @@
import { defineMessages } from '@edx/frontend-platform/i18n';
const messages = defineMessages({
warningMessage: {
id: 'ora-grading.demoAlert.warningMessage',
defaultMessage: 'Grade submission is disabled in the Demo mode of the new ORA Staff Grader.',
description: 'Submit Grade button text after successful submission',
},
confirm: {
id: 'ora-grading.demoAlert.confirm',
defaultMessage: 'Confirm',
description: 'Confirm button text',
},
title: {
id: 'ora-grading.demoAlert.title',
defaultMessage: 'Demo submit prevented',
description: 'Title of alert modal after submit was prevented because in demo mode',
},
});
export default messages;

View File

@@ -1,6 +1,6 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`FilePopoverContent component snapshot 1`] = `
exports[`FilePopoverContent component snapshot default 1`] = `
<Fragment>
<div
className="help-popover-option"
@@ -28,5 +28,62 @@ exports[`FilePopoverContent component snapshot 1`] = `
<br />
long descriptive text...
</div>
<div
className="help-popover-option"
>
<strong>
<FormattedMessage
defaultMessage="File Size"
description="Popover title for file size"
id="ora-grading.FilePopoverCellContent.fileSizeTitle"
/>
</strong>
<br />
filesize(6000)
</div>
</Fragment>
`;
exports[`FilePopoverContent component snapshot invalid size 1`] = `
<Fragment>
<div
className="help-popover-option"
>
<strong>
<FormattedMessage
defaultMessage="File Name"
description="Popover title for file name"
id="ora-grading.FilePopoverContent.filePopoverNameTitle"
/>
</strong>
<br />
some file name
</div>
<div
className="help-popover-option"
>
<strong>
<FormattedMessage
defaultMessage="File Description"
description="Popover title for file description"
id="ora-grading.FilePopoverCellContent.filePopoverDescriptionTitle"
/>
</strong>
<br />
long descriptive text...
</div>
<div
className="help-popover-option"
>
<strong>
<FormattedMessage
defaultMessage="File Size"
description="Popover title for file size"
id="ora-grading.FilePopoverCellContent.fileSizeTitle"
/>
</strong>
<br />
Unknown
</div>
</Fragment>
`;

View File

@@ -2,33 +2,39 @@ import React from 'react';
import PropTypes from 'prop-types';
import { FormattedMessage } from '@edx/frontend-platform/i18n';
import filesize from 'filesize';
import messages from './messages';
export const FilePopoverContent = ({ file }) => (
export const FilePopoverContent = ({ name, description, size }) => (
<>
<div className="help-popover-option">
<strong><FormattedMessage {...messages.filePopoverNameTitle} /></strong>
<br />
{file.name}
{name}
</div>
<div className="help-popover-option">
<strong><FormattedMessage {...messages.filePopoverDescriptionTitle} /></strong>
<br />
{file.description}
{description}
</div>
<div className="help-popover-option">
<strong><FormattedMessage {...messages.fileSizeTitle} /></strong>
<br />
{typeof (size) === 'number' ? filesize(size) : 'Unknown'}
</div>
</>
);
FilePopoverContent.defaultProps = {
description: '',
size: null,
};
FilePopoverContent.propTypes = {
file: PropTypes.shape({
name: PropTypes.string.isRequired,
description: PropTypes.string,
downloadURL: PropTypes.string,
}).isRequired,
name: PropTypes.string.isRequired,
description: PropTypes.string,
size: PropTypes.number,
};
export default FilePopoverContent;

View File

@@ -1,29 +1,38 @@
import React from 'react';
import { shallow } from 'enzyme';
import filesize from 'filesize';
import FilePopoverContent from '.';
jest.mock('filesize', () => (size) => `filesize(${size})`);
describe('FilePopoverContent', () => {
describe('component', () => {
const props = {
file: {
name: 'some file name',
description: 'long descriptive text...',
downloadURL: 'this-url-is.working',
},
name: 'some file name',
description: 'long descriptive text...',
downloadURL: 'this-url-is.working',
size: 6000,
};
let el;
beforeEach(() => {
el = shallow(<FilePopoverContent {...props} />);
});
test('snapshot', () => {
expect(el).toMatchSnapshot();
describe('snapshot', () => {
test('default', () => expect(el).toMatchSnapshot());
test('invalid size', () => {
el.setProps({
size: null,
});
expect(el).toMatchSnapshot();
});
});
describe('behavior', () => {
test('content', () => {
expect(el.text()).toContain(props.file.name);
expect(el.text()).toContain(props.file.description);
expect(el.text()).toContain(props.name);
expect(el.text()).toContain(props.description);
expect(el.text()).toContain(filesize(props.size));
});
});
});

View File

@@ -11,6 +11,11 @@ const messages = defineMessages({
defaultMessage: 'File Description',
description: 'Popover title for file description',
},
fileSizeTitle: {
id: 'ora-grading.FilePopoverCellContent.fileSizeTitle',
defaultMessage: 'File Size',
description: 'Popover title for file size',
},
});
export default messages;

View File

@@ -0,0 +1,44 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Alert, Button } from '@edx/paragon';
import { Info } from '@edx/paragon/icons';
import { FormattedMessage } from '@edx/frontend-platform/i18n';
const messageShape = PropTypes.shape({
id: PropTypes.string,
defaultMessage: PropTypes.string,
});
export const ErrorBanner = ({ actions, headingMessage, children }) => {
const actionButtons = actions.map(action => (
<Button key={action.id} onClick={action.onClick} variant="outline-primary">
<FormattedMessage {...action.message} />
</Button>
));
return (
<Alert variant="danger" icon={Info} actions={actionButtons}>
<Alert.Heading>
<FormattedMessage {...headingMessage} />
</Alert.Heading>
{children}
</Alert>
);
};
ErrorBanner.defaultProps = {
actions: [],
children: null,
};
ErrorBanner.propTypes = {
actions: PropTypes.arrayOf(
PropTypes.shape({
id: PropTypes.string,
onClick: PropTypes.func,
message: messageShape,
}),
),
headingMessage: messageShape.isRequired,
children: PropTypes.node,
};
export default ErrorBanner;

View File

@@ -0,0 +1,59 @@
import React from 'react';
import { shallow } from 'enzyme';
import ErrorBanner from './ErrorBanner';
import messages from '../messages';
describe('Error Banner component', () => {
const children = <p>Abitary Child</p>;
const props = {
actions: [
{
id: 'action1',
onClick: jest.fn().mockName('action1.onClick'),
message: messages.retryButton,
},
{
id: 'action2',
onClick: jest.fn().mockName('action2.onClick'),
message: messages.retryButton,
},
],
headingMessage: messages.unknownError,
children,
};
let el;
beforeEach(() => {
el = shallow(<ErrorBanner {...props} />);
});
test('snapshot', () => {
expect(el).toMatchSnapshot();
});
describe('component', () => {
test('children node', () => {
expect(el.containsMatchingElement(children)).toEqual(true);
});
test('verify actions', () => {
const actions = el.find('Alert').prop('actions');
expect(actions).toHaveLength(props.actions.length);
actions.forEach((action, index) => {
expect(action.type).toEqual('Button');
expect(action.props.onClick).toEqual(props.actions[index].onClick);
// action message
expect(action.props.children.props).toEqual(props.actions[index].message);
});
});
test('verify heading', () => {
const heading = el.find('FormattedMessage');
expect(heading.props()).toEqual(props.headingMessage);
});
});
});

View File

@@ -0,0 +1,14 @@
import React from 'react';
import { Alert, Spinner } from '@edx/paragon';
export const LoadingBanner = () => (
<Alert variant="info">
<Spinner animation="border" className="d-flex m-auto" />
</Alert>
);
LoadingBanner.defaultProps = {};
LoadingBanner.propTypes = {};
export default LoadingBanner;

View File

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

View File

@@ -0,0 +1,42 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Error Banner component snapshot 1`] = `
<Alert
actions={
Array [
<Button
onClick={[MockFunction action1.onClick]}
variant="outline-primary"
>
<FormattedMessage
defaultMessage="Retry"
description="Retry button for error in file renderer"
id="ora-grading.ResponseDisplay.FileRenderer.retryButton"
/>
</Button>,
<Button
onClick={[MockFunction action2.onClick]}
variant="outline-primary"
>
<FormattedMessage
defaultMessage="Retry"
description="Retry button for error in file renderer"
id="ora-grading.ResponseDisplay.FileRenderer.retryButton"
/>
</Button>,
]
}
variant="danger"
>
<Alert.Heading>
<FormattedMessage
defaultMessage="Unknown errors"
description="Unknown errors message"
id="ora-grading.ResponseDisplay.FileRenderer.unknownError"
/>
</Alert.Heading>
<p>
Abitary Child
</p>
</Alert>
`;

View File

@@ -0,0 +1,12 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Loading Banner component snapshot 1`] = `
<Alert
variant="info"
>
<Spinner
animation="border"
className="d-flex m-auto"
/>
</Alert>
`;

View File

@@ -0,0 +1,2 @@
export { default as ErrorBanner } from './ErrorBanner';
export { default as LoadingBanner } from './LoadingBanner';

View File

@@ -0,0 +1,27 @@
import React from 'react';
import PropTypes from 'prop-types';
const ImageRenderer = ({
url, fileName, onError, onSuccess,
}) => (
<img
alt={fileName}
className="image-renderer"
src={url}
onError={onError}
onLoad={onSuccess}
/>
);
ImageRenderer.defaultProps = {
fileName: '',
};
ImageRenderer.propTypes = {
url: PropTypes.string.isRequired,
fileName: PropTypes.string,
onError: PropTypes.func.isRequired,
onSuccess: PropTypes.func.isRequired,
};
export default ImageRenderer;

View File

@@ -8,6 +8,9 @@ describe('Image Renderer Component', () => {
url: 'some_url.jpg',
};
props.onError = jest.fn().mockName('this.props.onError');
props.onSuccess = jest.fn().mockName('this.props.onSuccess');
let el;
beforeEach(() => {
el = shallow(<ImageRenderer {...props} />);

View File

@@ -0,0 +1,92 @@
import React from 'react';
import PropTypes from 'prop-types';
import { pdfjs, Document, Page } from 'react-pdf';
import {
Icon, Form, ActionRow, IconButton,
} from '@edx/paragon';
import { ChevronLeft, ChevronRight } from '@edx/paragon/icons';
import pdfjsWorker from 'react-pdf/dist/esm/pdf.worker.entry';
import 'react-pdf/dist/esm/Page/AnnotationLayer.css';
import { rendererHooks } from './pdfHooks';
pdfjs.GlobalWorkerOptions.workerSrc = pdfjsWorker;
/**
* <PDFRenderer />
*/
export const PDFRenderer = ({
onError,
onSuccess,
url,
}) => {
const {
pageNumber,
numPages,
relativeHeight,
wrapperRef,
onDocumentLoadSuccess,
onLoadPageSuccess,
onDocumentLoadError,
onInputPageChange,
onNextPageButtonClick,
onPrevPageButtonClick,
hasNext,
hasPrev,
} = rendererHooks({ onError, onSuccess });
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}
/>
<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>
);
};
PDFRenderer.defaultProps = {};
PDFRenderer.propTypes = {
url: PropTypes.string.isRequired,
onError: PropTypes.func.isRequired,
onSuccess: PropTypes.func.isRequired,
};
export default PDFRenderer;

View File

@@ -0,0 +1,57 @@
import React from 'react';
import { shallow } from 'enzyme';
import PDFRenderer from './PDFRenderer';
import * as hooks from './pdfHooks';
jest.mock('react-pdf', () => ({
pdfjs: { GlobalWorkerOptions: {} },
Document: () => 'Document',
Page: () => 'Page',
}));
jest.mock('./pdfHooks', () => ({
rendererHooks: jest.fn(),
}));
describe('PDF Renderer Component', () => {
const props = {
url: 'some_url.pdf',
onError: jest.fn().mockName('this.props.onError'),
onSuccess: jest.fn().mockName('this.props.onSuccess'),
};
const hookProps = {
pageNumber: 1,
numPages: 10,
relativeHeight: 200,
wrapperRef: { current: 'hooks.wrapperRef' },
onDocumentLoadSuccess: jest.fn().mockName('hooks.onDocumentLoadSuccess'),
onLoadPageSuccess: jest.fn().mockName('hooks.onLoadPageSuccess'),
onDocumentLoadError: jest.fn().mockName('hooks.onDocumentLoadError'),
onInputPageChange: jest.fn().mockName('hooks.onInputPageChange'),
onNextPageButtonClick: jest.fn().mockName('hooks.onNextPageButtonClick'),
onPrevPageButtonClick: jest.fn().mockName('hooks.onPrevPageButtonClick'),
hasNext: true,
hasPref: false,
};
beforeEach(() => {
jest.clearAllMocks();
});
describe('snapshots', () => {
test('first page, prev is disabled', () => {
hooks.rendererHooks.mockReturnValue(hookProps);
expect(shallow(<PDFRenderer {...props} />)).toMatchSnapshot();
});
test('on last page, next is disabled', () => {
hooks.rendererHooks.mockReturnValue({
...hookProps,
pageNumber: hookProps.numPages,
hasNext: false,
hasPrev: true,
});
expect(shallow(<PDFRenderer {...props} />)).toMatchSnapshot();
});
});
});

View File

@@ -0,0 +1,22 @@
import React from 'react';
import PropTypes from 'prop-types';
import { rendererHooks } from './textHooks';
const TXTRenderer = ({ url, onError, onSuccess }) => {
const { content } = rendererHooks({ url, onError, onSuccess });
return (
<pre className="txt-renderer">
{content}
</pre>
);
};
TXTRenderer.defaultProps = {};
TXTRenderer.propTypes = {
url: PropTypes.string.isRequired,
onError: PropTypes.func.isRequired,
onSuccess: PropTypes.func.isRequired,
};
export default TXTRenderer;

View File

@@ -0,0 +1,23 @@
import React from 'react';
import { shallow } from 'enzyme';
import TXTRenderer from './TXTRenderer';
jest.mock('./textHooks', () => {
const content = 'test-content';
return {
content,
rendererHooks: (args) => ({ content, rendererHooks: 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'),
};
test('snapshot', () => {
expect(shallow(<TXTRenderer {...props} />)).toMatchSnapshot();
});
});

View File

@@ -4,6 +4,8 @@ exports[`Image Renderer Component snapshot 1`] = `
<img
alt=""
className="image-renderer"
onError={[MockFunction this.props.onError]}
onLoad={[MockFunction this.props.onSuccess]}
src="some_url.jpg"
/>
`;

View File

@@ -0,0 +1,137 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`PDF Renderer Component snapshots first page, prev 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={1}
/>
</div>
</Document>
<ActionRow
className="d-flex justify-content-center m-0"
>
<IconButton
alt="previous pdf page"
disabled={true}
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={1}
/>
<Form.Label
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
</Form.Label>
</Form.Group>
<IconButton
alt="next pdf page"
disabled={true}
iconAs="Icon"
onClick={[MockFunction hooks.onNextPageButtonClick]}
size="inline"
src={[MockFunction icons.ChevronRight]}
/>
</ActionRow>
</div>
`;

View File

@@ -1,9 +1,9 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Image Renderer Component snapshot 1`] = `
exports[`TXT Renderer Component snapshot 1`] = `
<pre
className="txt-renderer"
>
Content of some_url.txt
test-content
</pre>
`;

View File

@@ -0,0 +1,3 @@
export { default as ImageRenderer } from './ImageRenderer';
export { default as PDFRenderer } from './PDFRenderer';
export { default as TXTRenderer } from './TXTRenderer';

View File

@@ -0,0 +1,81 @@
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

@@ -0,0 +1,148 @@
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

@@ -0,0 +1,34 @@
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

@@ -0,0 +1,94 @@
/* 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,9 +12,13 @@ import './FileCard.scss';
*/
export const FileCard = ({ file, children }) => (
<Card className="file-card" key={file.name}>
<Collapsible className="file-collapsible" defaultOpen title={<h3>{file.name}</h3>}>
<Collapsible
className="file-collapsible"
defaultOpen
title={<h3 className="file-card-title">{file.name}</h3>}
>
<div className="preview-panel">
<FileInfo><FilePopoverContent file={file} /></FileInfo>
<FileInfo><FilePopoverContent {...file} /></FileInfo>
{children}
</div>
</Collapsible>

View File

@@ -2,6 +2,12 @@
.file-card {
margin: map-get($spacers, 1) 0;
.file-card-title {
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
}
}
.image-renderer {
@@ -18,4 +24,10 @@
.txt-renderer {
white-space: pre-wrap;
}
@include media-breakpoint-down(sm) {
.file-card-title {
width: map-get($container-max-widths, "sm")/2;
}
}

View File

@@ -29,7 +29,7 @@ describe('File Preview Card component', () => {
describe('Component', () => {
test('collapsible title is name header', () => {
const title = el.find(Collapsible).prop('title');
expect(title).toEqual(<h3>{props.file.name}</h3>);
expect(title).toEqual(<h3 className="file-card-title">{props.file.name}</h3>);
});
test('forwards children into preview-panel', () => {
const previewPanelChildren = el.find('.preview-panel').children();

View File

@@ -8,6 +8,7 @@ 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';
/**
@@ -19,13 +20,13 @@ export const FileInfo = ({ onClick, children }) => (
placement="right-end"
flip
overlay={(
<Popover className="overlay-help-popover">
<Popover id="file-popover" className="overlay-help-popover">
<Popover.Content>{children}</Popover.Content>
</Popover>
)}
>
<Button
size="small"
size="sm"
variant="tertiary"
onClick={onClick}
iconAfter={InfoOutline}
@@ -36,7 +37,7 @@ export const FileInfo = ({ onClick, children }) => (
);
FileInfo.defaultProps = {
onClick: () => {},
onClick: nullMethod,
};
FileInfo.propTypes = {
onClick: PropTypes.func,

View File

@@ -0,0 +1,46 @@
import React from 'react';
import PropTypes from 'prop-types';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import FileCard from './FileCard';
import { ErrorBanner, LoadingBanner } from './Banners';
import { renderHooks } from './hooks';
/**
* <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>
);
};
FileRenderer.defaultProps = {};
FileRenderer.propTypes = {
file: PropTypes.shape({
name: PropTypes.string,
downloadUrl: PropTypes.string,
}).isRequired,
// injected
intl: intlShape.isRequired,
};
export default injectIntl(FileRenderer);

View File

@@ -0,0 +1,53 @@
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';
jest.mock('./FileCard', () => 'FileCard');
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', () => {
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();
});
test('is not loading, with error', () => {
const hookProps = {
Renderer: () => 'Renderer',
isloading: false,
errorStatus: ErrorStatuses.serverError,
error: { prop: 'hooks.errorProps' },
rendererProps: { prop: 'hooks.rendererProps' },
};
jest.spyOn(hooks, hookKeys.renderHooks).mockReturnValueOnce(hookProps);
expect(shallow(<FileRenderer {...props} />)).toMatchSnapshot();
});
});
});
});

View File

@@ -1,15 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
const ImageRenderer = ({ url, fileName }) => (<img alt={fileName} className="image-renderer" src={url} />);
ImageRenderer.defaultProps = {
fileName: '',
};
ImageRenderer.propTypes = {
url: PropTypes.string.isRequired,
fileName: PropTypes.string,
};
export default ImageRenderer;

View File

@@ -1,145 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
import { pdfjs, Document, Page } from 'react-pdf';
import {
Icon, Form, ActionRow, IconButton,
} from '@edx/paragon';
import { ChevronLeft, ChevronRight } from '@edx/paragon/icons';
import pdfjsWorker from 'react-pdf/node_modules/pdfjs-dist/build/pdf.worker.entry';
import 'react-pdf/dist/esm/Page/AnnotationLayer.css';
pdfjs.GlobalWorkerOptions.workerSrc = pdfjsWorker;
/**
* <PDFRenderer />
*/
export class PDFRenderer extends React.Component {
static INITIAL_STATE = {
pageNumber: 1,
numPages: 1,
relativeHeight: 0,
};
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.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) => {
// eslint-disable-next-line no-console
console.error(error);
};
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.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 = {};
PDFRenderer.propTypes = {
url: PropTypes.string.isRequired,
};
export default PDFRenderer;

View File

@@ -1,218 +0,0 @@
import React from 'react';
import { shallow } from 'enzyme';
import { Document, Page } from 'react-pdf';
import { Form, IconButton } from '@edx/paragon';
import PDFRenderer from './PDFRenderer';
jest.mock('react-pdf', () => ({
pdfjs: { GlobalWorkerOptions: {} },
Document: () => 'Document',
Page: () => 'Page',
}));
describe('PDF Renderer Component', () => {
const props = {
url: 'some_url.pdf',
};
let el;
describe('snapshots', () => {
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('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);
});
});
});
});
});

View File

@@ -1,24 +0,0 @@
import React, { useMemo, useState } from 'react';
import PropTypes from 'prop-types';
import { get } from 'data/services/lms/utils';
const TXTRenderer = ({ url }) => {
const [content, setContent] = useState('');
useMemo(() => {
get(url).then(({ data }) => setContent(data));
}, [url]);
return (
<pre className="txt-renderer">
{content}
</pre>
);
};
TXTRenderer.defaultProps = {};
TXTRenderer.propTypes = {
url: PropTypes.string.isRequired,
};
export default TXTRenderer;

View File

@@ -1,22 +0,0 @@
import React from 'react';
import { shallow } from 'enzyme';
import TXTRenderer from './TXTRenderer';
jest.mock('data/services/lms/utils', () => ({
get: jest.fn((...args) => Promise.resolve({ data: `Content of ${args}` })),
}));
describe('Image Renderer Component', () => {
const props = {
url: 'some_url.txt',
};
let el;
beforeEach(() => {
el = shallow(<TXTRenderer {...props} />);
});
test('snapshot', () => {
expect(el).toMatchSnapshot();
});
});

View File

@@ -9,7 +9,9 @@ exports[`File Preview Card component snapshot 1`] = `
className="file-collapsible"
defaultOpen={true}
title={
<h3>
<h3
className="file-card-title"
>
test-file-name.pdf
</h3>
}
@@ -19,13 +21,9 @@ exports[`File Preview Card component snapshot 1`] = `
>
<FileInfo>
<FilePopoverContent
file={
Object {
"description": "test-file description",
"downloadUrl": "destination/test-file-name.pdf",
"name": "test-file-name.pdf",
}
}
description="test-file description"
downloadUrl="destination/test-file-name.pdf"
name="test-file-name.pdf"
/>
</FileInfo>
<h1>

View File

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

View File

@@ -0,0 +1,33 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`FileRenderer component snapshot is not loading, with error 1`] = `
<FileCard
file={
Object {
"downloadUrl": "file download url",
"name": "filename.txt",
}
}
key="file download url"
>
<ErrorBanner
prop="hooks.errorProps"
/>
</FileCard>
`;
exports[`FileRenderer component snapshot isLoading, no Error 1`] = `
<FileCard
file={
Object {
"downloadUrl": "file download url",
"name": "filename.txt",
}
}
key="file download url"
>
<Renderer
prop="hooks.rendererProps"
/>
</FileCard>
`;

View File

@@ -1,69 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`PDF Renderer Component snapshots snapshot 1`] = `
<div
className="pdf-renderer"
>
<Document
file="some_url.pdf"
onLoadError={[MockFunction onDocumentLoadError]}
onLoadSuccess={[MockFunction onDocumentLoadSuccess]}
>
<div
className="page-wrapper"
style={
Object {
"height": 0,
}
}
>
<Page
onLoadSuccess={[MockFunction onLoadPageSuccess]}
pageNumber={1}
/>
</div>
</Document>
<ActionRow
className="d-flex justify-content-center m-0"
>
<IconButton
alt="previous pdf page"
disabled={true}
iconAs="Icon"
onClick={[MockFunction 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={1}
min={0}
onChange={[MockFunction onInputPageChange]}
type="number"
value={1}
/>
<Form.Label
isInline={true}
>
of
1
</Form.Label>
</Form.Group>
<IconButton
alt="next pdf page"
disabled={true}
iconAs="Icon"
onClick={[MockFunction onNextPageButtonClick]}
size="inline"
src={[MockFunction icons.ChevronRight]}
/>
</ActionRow>
</div>
`;

View File

@@ -0,0 +1,102 @@
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

@@ -0,0 +1,117 @@
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,4 +1,2 @@
export { default as FileCard } from './FileCard';
export { default as ImageRenderer } from './ImageRenderer';
export { default as PDFRenderer } from './PDFRenderer';
export { default as TXTRenderer } from './TXTRenderer';
export { default as FileRenderer } from './FileRenderer';
export { isSupported } from './hooks';

View File

@@ -6,6 +6,21 @@ const messages = defineMessages({
defaultMessage: 'File info',
description: 'Popover trigger button text for file preview card',
},
retryButton: {
id: 'ora-grading.ResponseDisplay.FileRenderer.retryButton',
defaultMessage: 'Retry',
description: 'Retry button for error in file renderer',
},
fileNotFoundError: {
id: 'ora-grading.ResponseDisplay.FileRenderer.fileNotFound',
defaultMessage: 'File not found',
description: 'File not found error message',
},
unknownError: {
id: 'ora-grading.ResponseDisplay.FileRenderer.unknownError',
defaultMessage: 'Unknown errors',
description: 'Unknown errors message',
},
});
export default messages;

View File

@@ -0,0 +1,14 @@
// 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

@@ -0,0 +1,20 @@
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

@@ -0,0 +1,25 @@
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

@@ -0,0 +1,11 @@
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,6 +6,7 @@ exports[`Info Popover Component snapshot 1`] = `
overlay={
<Popover
className="overlay-help-popover"
id="info-popover"
>
<Popover.Content>
<div>

View File

@@ -10,6 +10,8 @@ import {
import { InfoOutline } from '@edx/paragon/icons';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { nullMethod } from 'hooks';
import messages from './messages';
/**
@@ -21,7 +23,7 @@ export const InfoPopover = ({ onClick, children, intl }) => (
placement="right-end"
flip
overlay={(
<Popover className="overlay-help-popover">
<Popover id="info-popover" className="overlay-help-popover">
<Popover.Content>{children}</Popover.Content>
</Popover>
)}
@@ -37,7 +39,7 @@ export const InfoPopover = ({ onClick, children, intl }) => (
);
InfoPopover.defaultProps = {
onClick: () => {},
onClick: nullMethod,
};
InfoPopover.propTypes = {
onClick: PropTypes.func,

View File

@@ -4,21 +4,29 @@ 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 statusVariants = {
[statuses.ungraded]: 'primary',
[statuses.locked]: 'light',
[statuses.graded]: 'success',
[statuses.inProgress]: 'warning',
};
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,
});
/**
* <StatusBadge />
*/
export const StatusBadge = ({ className, status }) => {
if (statusVariants[status] === undefined) {
if (!Object.keys(statusVariants).includes(status)) {
return null;
}
return (

View File

@@ -0,0 +1,35 @@
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={[Function]}
onClose={[MockFunction hooks.nullMethod]}
title="test-title"
>
<p>
@@ -49,7 +49,7 @@ exports[`ConfirmModal snapshot: open 1`] = `
</ActionRow>
}
isOpen={true}
onClose={[Function]}
onClose={[MockFunction hooks.nullMethod]}
title="test-title"
>
<p>

View File

@@ -0,0 +1,55 @@
// 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>
`;

View File

@@ -1,16 +0,0 @@
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

@@ -0,0 +1,11 @@
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

@@ -0,0 +1,31 @@
// 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

@@ -0,0 +1,29 @@
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

@@ -0,0 +1,23 @@
/* 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

@@ -1,38 +0,0 @@
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

@@ -1,24 +0,0 @@
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

@@ -1,27 +0,0 @@
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

@@ -1,23 +0,0 @@
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

@@ -1,40 +0,0 @@
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

@@ -1,24 +0,0 @@
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

@@ -1,31 +0,0 @@
// 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

@@ -1,28 +0,0 @@
// 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

@@ -1,22 +0,0 @@
// 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>
`;

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