Compare commits

...

145 Commits

Author SHA1 Message Date
dependabot[bot]
a853f8ce0c build(deps): bump nth-check and truncate-html
Bumps [nth-check](https://github.com/fb55/nth-check) to 2.1.1 and updates ancestor dependency [truncate-html](https://github.com/oe/truncate-html). These dependencies need to be updated together.


Updates `nth-check` from 1.0.2 to 2.1.1
- [Release notes](https://github.com/fb55/nth-check/releases)
- [Commits](https://github.com/fb55/nth-check/compare/v1.0.2...v2.1.1)

Updates `truncate-html` from 1.0.4 to 1.1.1
- [Release notes](https://github.com/oe/truncate-html/releases)
- [Commits](https://github.com/oe/truncate-html/compare/v1.0.4...v1.1.1)

---
updated-dependencies:
- dependency-name: nth-check
  dependency-type: indirect
- dependency-name: truncate-html
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-10-03 21:37:33 +00:00
Braden MacDonald
d64a4e448b chore(deps): bump sass to v1.79 and sass-loader to v16 (#1490) 2024-10-03 14:36:01 -07:00
Piotr Surowiec
860b3f9952 fix: send XBlock visibility status to the LMS (#1491) 2024-10-01 20:38:19 +05:30
edX requirements bot
4418c5422f chore: update browserslist DB (#1493)
Co-authored-by: abdullahwaheed <42172960+abdullahwaheed@users.noreply.github.com>
2024-09-30 00:36:11 +00:00
dependabot[bot]
372c9de1db chore(deps): bump micromatch from 4.0.5 to 4.0.8 (#1469)
Bumps [micromatch](https://github.com/micromatch/micromatch) from 4.0.5 to 4.0.8.
- [Release notes](https://github.com/micromatch/micromatch/releases)
- [Changelog](https://github.com/micromatch/micromatch/blob/master/CHANGELOG.md)
- [Commits](https://github.com/micromatch/micromatch/compare/4.0.5...4.0.8)

---
updated-dependencies:
- dependency-name: micromatch
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-09-23 12:14:18 -07:00
dependabot[bot]
65adaf18d4 build(deps): bump webpack from 5.89.0 to 5.94.0 (#1468)
Bumps [webpack](https://github.com/webpack/webpack) from 5.89.0 to 5.94.0.
- [Release notes](https://github.com/webpack/webpack/releases)
- [Commits](https://github.com/webpack/webpack/compare/v5.89.0...v5.94.0)

---
updated-dependencies:
- dependency-name: webpack
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-09-23 19:11:35 +00:00
renovate[bot]
ed77465282 chore(deps): update dependency joi to v17.13.3 (#1474) 2024-09-23 12:04:09 -07:00
edX requirements bot
f5f6747ecb chore: update browserslist DB (#1489)
Co-authored-by: abdullahwaheed <42172960+abdullahwaheed@users.noreply.github.com>
2024-09-23 11:42:30 -07:00
Ishan Masdekar
fbe16483ac fix: corrects navigation if the student does not pass the entrance exam (#1429) 2024-09-23 11:41:52 -07:00
Piotr Surowiec
e4a0105042 fix: increase subsection grades rounding precision (#1397)
We used two decimal digits to match the experience from the edx-platform.
However, https://github.com/openedx/edx-platform/pull/27788 increased
the precision to reduce the impact of double rounding.
2024-09-23 11:39:15 -07:00
Kaustav Banerjee
1d19ae0e7b fix: masquerade dropdown not showing current selection (#1434)
* feat: remove child components from state and use data instead

* fix: change active selection based on user input

* test: add test cases
2024-09-23 11:32:41 -07:00
Navin Karkera
b9d11982e3 feat: support jumping to specific xblock id (#1427)
Adds ability to pass `jumpToId` query param to iframe url as id hash to
be used by browser to scroll to the correct xblock
2024-09-20 10:34:56 -07:00
Braden MacDonald
73590f1ccd fix: upload codecov report as a separate workflow step (#1476) 2024-09-20 10:21:33 -07:00
Braden MacDonald
f8d35bf45d docs: Update README to explain how to run this using Tutor (#1472) 2024-09-19 09:55:48 -07:00
Braden MacDonald
2038bad822 feat: use bundlewatch to monitor bundle size (#1479) 2024-09-19 16:15:15 +00:00
Braden MacDonald
6c11947397 fix: sass deprecation warning in GradeBar.scss (#1473) 2024-09-19 09:06:11 -07:00
renovate[bot]
26565cd89c chore(deps): update dependency axios-mock-adapter to v2 (#1484)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-09-17 11:28:34 -07:00
edX requirements bot
3a203e8351 chore: update browserslist DB (#1487)
Co-authored-by: abdullahwaheed <42172960+abdullahwaheed@users.noreply.github.com>
2024-09-16 09:48:48 -07:00
Kristin Aoki
500e4abcb9 Revert "fix: show studio button if user has access (#1452)" (#1486)
This reverts commit 82b27e59cc.
2024-09-13 13:39:10 -04:00
renovate[bot]
d78851bb5b chore(deps): update dependency @pact-foundation/pact to v13 (#1478)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-09-12 22:12:41 +00:00
renovate[bot]
8c9a43d02b chore(deps): update actions/checkout action to v4 (#1477)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-09-12 15:06:53 -07:00
renovate[bot]
13c7c1de89 fix(deps): update dependency classnames to v2.5.1 (#1464)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-09-12 13:55:42 -07:00
renovate[bot]
ba44b28cec fix(deps): update dependency @openedx/paragon to v22.8.1 2024-09-12 19:18:16 +00:00
renovate[bot]
79affe0629 chore(deps): update dependency axios-mock-adapter to v1.22.0 (#1037)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-09-12 19:10:17 +00:00
edX requirements bot
d0ec7e3fb2 chore: enable github action auto update in dependabot.yml (#1451) 2024-09-12 11:48:37 -07:00
renovate[bot]
3f8b8077a9 chore(deps): update dependency @testing-library/jest-dom to v5.17.0 (#1362)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-09-12 11:47:07 -07:00
renovate[bot]
290f17d76d fix(deps): update dependency @openedx/frontend-plugin-framework to v1.3.0 2024-09-12 18:46:31 +00:00
renovate[bot]
64a1149550 fix(deps): update dependency @edx/frontend-platform to v8.1.1 2024-09-12 18:46:16 +00:00
edX requirements bot
e907ade40a chore: update browserslist DB (#1447)
Co-authored-by: abdullahwaheed <42172960+abdullahwaheed@users.noreply.github.com>
2024-09-12 11:38:51 -07:00
renovate[bot]
68a7bf5527 fix(deps): update dependency @openedx/frontend-slot-footer to v1.0.5 2024-09-12 16:21:21 +00:00
renovate[bot]
db75ea28e4 fix(deps): update dependency @edx/openedx-atlas to v0.6.2 2024-09-12 16:20:58 +00:00
Braden MacDonald
ba4bdfe6af fix: remove unneeded deps, put build deps into 'dependencies' not dev (#1456) 2024-09-12 09:14:56 -07:00
Jorg Are
b63508db97 feat: Add a plugin slot for the content iframe loader (#1453) 2024-09-11 14:11:08 +01:00
Kristin Aoki
82b27e59cc fix: show studio button if user has access (#1452) 2024-09-09 09:11:56 -04:00
Bilal Qamar
ec8b5c5d6e build: Upgrade to Node 20 (#1443)
* feat: updated node to v20

* fix: updated/resolved failing test

* refactor: updated package-lock along with validate & lockfile version workflows

* refactor: removed unnecesary eslint ignore & updated ProductTours test
2024-09-06 12:23:07 -04:00
Bilal Qamar
dc1e9cd2e8 test: Add Node 20 to CI matrix (#1445)
* test: Add Node 20 to CI matrix

* refactor: removed eslint disable

* refactor: updated validate workflow continue-on-error node version
2024-09-03 12:20:36 -04:00
Muhammad Adeel Tajamul
a681333a08 feat: added UI for one click unsubscribe flow (#1444)
* feat: added UI for one click unsubscribe flow

---------

Co-authored-by: Awais Ansari <awais.ansari63@gmail.com>
2024-08-19 14:15:15 +05:00
renovate[bot]
7cbbc720d1 chore(deps): update dependency @openedx/frontend-build to v14.1.0 2024-08-15 03:10:59 +00:00
renovate[bot]
863a838e6e fix(deps): update dependency @openedx/frontend-slot-footer to v1.0.3 2024-08-15 00:29:50 +00:00
renovate[bot]
5b046e828a fix(deps): update dependency @openedx/frontend-plugin-framework to v1.2.3 2024-08-14 22:00:05 +00:00
renovate[bot]
a8f72c5e75 fix(deps): update dependency @edx/openedx-atlas to v0.6.1 2024-08-14 20:28:08 +00:00
renovate[bot]
bb6c678904 fix(deps): update dependency @edx/frontend-component-header to v5.3.4 2024-08-14 15:27:41 +00:00
Bilal Qamar
71c2a31531 feat: updated frontend-build & frontend-platform major versions (#1391)
* feat: platform & react-unit-test-utils major version update, updated jest to v29

* feat: updated frontend-build to v14 along with respective edx packages

* refactor: bumped package versions, updated snapshots for failing tests

* fix: code refactors to resolve failing tests

* refactor: added code comment in jest config
2024-08-14 11:20:27 -04:00
Braden MacDonald
99a44dda37 docs: transfer maintainership from 2U Aurora to volunteers (Braden + Farhaan) 2024-08-08 08:27:24 -04:00
Marcos Rigoli
5ae86465cc fix: Optimizely initialization refactor for Xpert (#1432) 2024-08-06 14:01:12 -03:00
Marcos Rigoli
6e9c105eb9 fix: Updated learning assistant version (#1431) 2024-08-05 10:10:16 -04:00
sundasnoreen12
26199fa954 fix: reset initialsidebar after shouldDisplaySidebarOpen (#1428)
* fix: reset initialsidebar after shouldDisplaySidebarOpen

* fix: fixed issue due to localstorage sidebar value when shifting from old to new one
2024-08-02 15:22:18 +05:00
Arunmozhi
3a542766d7 fix: remove redundant form-control in masquerade user input 2024-08-01 11:18:57 -04:00
sundasnoreen12
bbe03dc46f fix: fixed overflow issue of stacked bar on mobile (#1425)
* fix: fixed overflow issue of stacked bar on mobile

* refactor: instead of ismobileview i used shouldDisplayFullScreen
2024-07-30 13:53:12 +05:00
Ihor Romaniuk
167d51b596 fix: iframe height for discussions sidebar (#1393)
* fix: iframe height for discussions sidebar

* fix: increase adaptation brakepoint
2024-07-26 10:57:40 -04:00
sundasnoreen12
7efe8f5cc3 chore: make sidebar less intrusive for mobile (#1423)
* chore: make sidebar less intrusive for mobile

* fix: fixed first time view issue

* refactor: refactor code
2024-07-24 14:59:53 +05:00
Alison Langston
263fe6d1a2 chore: upgrade la library version (#1424) 2024-07-23 08:57:54 -04:00
Marcos Rigoli
76f98d5bb2 feat: Added experiments to Learning Assistant (#1421) 2024-07-19 12:33:13 -03:00
Zachary Hancock
e0386fe40b fix: update exams lib (#1422) 2024-07-19 10:56:13 -04:00
Kristin Aoki
7d99677acd feat: plugin slot to show non course content on next button click (#1412) 2024-07-17 21:05:43 -06:00
Marcos Rigoli
29bc2d9e17 chore: Revert Learning Assistant upgrade due to a bug (#1420) 2024-07-15 11:49:39 -03:00
Marcos Rigoli
27f3e79508 chore: Update learning assistant lib (#1419) 2024-07-15 11:10:55 -03:00
Zachary Hancock
ed74bee760 chore: update exams lib (#1418) 2024-07-11 14:30:03 -04:00
Emad Rad
c7a81fe07a chore: README updated
- code blocks added.
- heading issues fixed.
- small typos fixed.
2024-06-28 09:09:39 -04:00
Zachary Hancock
d880aac569 chore: update learning assistant lib (#1411) 2024-06-24 09:04:08 -04:00
Rodrigo Martin
072d608c64 feat(AU-2073): Trigger track event when iframe fails on load (#1407)
* feat(AU-2073): Trigger track event when iframe fails on load

* feat: fix tests

* fix: remove courseId from event
2024-06-04 16:48:21 -03:00
Ihor Romaniuk
9437142bc8 fix: optimize scroll position observer after video fullscreen exit (#1371) 2024-05-30 14:15:15 -03:00
Ihor Romaniuk
58c8ec5777 feat: [FC-0056] courseware sidebar enhancement (#1386)
- Display section and sequence progress
- Add tracking event to the unit button
- Hide the horizontal unit navigation with enabled sidebar navigation
2024-05-30 13:26:59 -03:00
Ahtisham Shahid
07357b9f10 feat: added role attrs in sidebar tracking event (#1399) 2024-05-30 12:06:21 +05:00
sundasnoreen12
1264b4245c fix: fixed width issue of incontext sidebar (#1395) 2024-05-27 13:43:31 +05:00
Zachary Hancock
e3ecee18e3 style: use brand style for search button border (#1394) 2024-05-23 13:08:48 -04:00
Varsha Menon
c3d96622e8 feat: update courseware search button (#1382) 2024-05-23 11:27:10 -04:00
Jorg Are
e577efbd27 feat: add unit title to pluginslot (#1383) 2024-05-21 17:20:51 +01:00
Rodrigo Martin
df361236d0 feat(AU-2006): Show loading state when translating unit block (#1389) 2024-05-17 13:48:23 -03:00
Brian Smith
e656f5445c feat!: organize plugin slots as components, add footer slot (#1381)
BREAKING CHANGE: slot ids have been changed for consistency
* `sequence_container_plugin` -> `sequence_container_slot`
* `unit_title_plugin` -> `unit_title_slot`
2024-05-17 12:09:47 -03:00
Zachary Hancock
f124c0d491 fix: incorrectly named plugin slots (#1388) 2024-05-16 10:24:27 -04:00
Leangseu Kim
cc041ba348 chore: lock test util to version 2.0.0 2024-05-15 15:42:24 -04:00
Leangseu Kim
257c9dcd7f chore: make sidebar less intrusive for mobile (#1377)
* chore: fix incorrect fetch result

* chore: make sidebar less intrusive for mobile

* chore: linting
2024-05-14 11:22:55 -04:00
Marcos Rigoli
1857b86c7e feat: Notification Plugin Slots (#1368)
* feat: add plugin slot for fbe lock paywall (#1347)

* feat: Added PluginSlot wrapping UpgradeNotification components (#1366)

* chore: Updated PluginSlot mock to support children and test ids

* chore: Updated mocked PluginSlot

* chore: Added unit test for MockedPluginSlot

* fix: Updated slot name ids

* feat: Added Plugin Slot wrapping UpgradeNotification in NotificationTray (#1367)

* fix: Removed PluginSlot prop scoping for UpgradeNotification (#1369)

* feat: generic sidebar notification plugin (#1379)

* feat: make notification plugin api generic

* feat: include new sidebar and tests

* feat: tweak sidebar toggle

* style: fix extra space from merge

* feat: rename plugin slots

---------

Co-authored-by: Alison Langston <46360176+alangsto@users.noreply.github.com>
Co-authored-by: Zachary Hancock <zhancock@edx.org>
2024-05-13 16:59:20 -04:00
Ihor Romaniuk
1c3610e9af feat: [FC-0056] create course outline sidebar (#1375) 2024-05-07 13:02:06 -03:00
Awais Ansari
796bbef10b fix: removed new sidebar view tickiness (#1376) 2024-04-30 19:09:17 +05:00
sundasnoreen12
799e57f970 fix: fixed width issues of old and new sidebar (#1374)
Co-authored-by: Awais Ansari <79941147+awais-ansari@users.noreply.github.com>
2024-04-30 17:40:23 +05:00
Awais Ansari
cf3a91dde0 feat: added course level isNewDiscussionSidebarViewEnabled flag to control sidebar view switching (#1373)
* feat: added course level isNewDiscussionSidebarViewEnabled flag

* test: fixed notification widget test cases
2024-04-29 16:14:08 +05:00
Leangseu Kim
72381a783b chore: remove UnitTranslationPlugin (#1352)
* chore: remove UnitTranslationPlugin

* chore: add plugin slot for sequence
2024-04-22 12:14:33 -04:00
sundasnoreen12
75f56ea4bd fix: fixed flicker issue of navbar width (#1364)
* fix: fixed fliker issue of navbar  width

* refactor: added hook function

* refactor: placed discussion and notification constant on common place

* refactor: moved to constant

* refactor: fixed variable rename
2024-04-18 15:27:16 +05:00
Rodrigo Martin
a418ba6adb feat(AU-1894): send track event when requesting translation (#1360)
* feat(AU-1894): send track event when requesting translation

* feat: add source != target language condition
2024-04-17 15:42:51 -03:00
sundasnoreen12
2da930f819 fix: fixed expended view unit bar issue (#1356)
* fix: fixed expended view unit bar issue

* refactor: removed expanded width
2024-04-17 12:29:45 +05:00
renovate[bot]
a2c38112fb chore(deps): update dependency @openedx/frontend-build to v13.1.4 2024-04-16 18:01:10 +00:00
Alison Langston
36535d188d feat: upgrade special exams (#1357) 2024-04-16 12:55:53 -04:00
renovate[bot]
79f49032e3 fix(deps): update dependency @edx/frontend-lib-special-exams to v3.0.1 2024-04-16 16:09:21 +00:00
renovate[bot]
9b3b123e45 fix(deps): update dependency @edx/frontend-component-footer to v13.0.5 2024-04-16 16:08:42 +00:00
sundasnoreen12
98436b4605 fix: fixed zindex and width issue (#1346) 2024-04-15 19:15:42 +00:00
Leangseu Kim
7652fa46d1 Lk/translation only for verified (#1355)
* chore: update verified mode logic

* chore: add is staff logic

* chore: add test
2024-04-15 11:57:20 -03:00
Leangseu Kim
2347ce88cd chore: update translation tour modal title 2024-04-15 09:40:22 -04:00
Leangseu Kim
78e5c57bd3 chore: fix translation for verified student only logic 2024-04-15 09:40:22 -04:00
Leangseu Kim
108636761c chore: fix codecov action error 2024-04-12 11:28:23 -04:00
Leangseu Kim
5f56828bda chore: patch frontend-build deployment 2024-04-12 11:28:23 -04:00
Rodrigo Martin
23e522e893 feat(AU-1949): Restrict WCT to verified track learners only (#1345)
* feat(AU-1949): Restrict WCT to verified track learners only

* fix: use jest-when

* fix: clean tests
2024-04-08 11:16:08 -03:00
sundasnoreen12
9423a889ba fix: fixed units overflow issue (#1343)
* fix: fixed units overflow issue

* refactor: refactor code for desktop and xl screen

* refactor: fixed refactor issue
2024-04-05 01:33:07 +05:00
Isaac Lee
85b95adb9f fix: modal iframe height to 100% (#1341) 2024-04-02 13:14:05 -04:00
ABBOUD Moncef
d4e7b416b1 fix: course update iframe (#1204) 2024-03-29 16:12:23 -04:00
renovate[bot]
0cb369af2b fix(deps): update dependency @openedx/paragon to v22.2.1 2024-03-27 20:57:21 +00:00
renovate[bot]
c908d3a3dd fix(deps): update dependency @edx/frontend-platform to v7.1.3 2024-03-27 17:48:28 +00:00
renovate[bot]
895a3b6b9f chore(deps): update dependency @openedx/frontend-build to v13.0.30 2024-03-27 17:47:25 +00:00
Leangseu Kim
36b3c36379 feat: whole course translations (#1330)
* feat: add language selection

chore: update tests so we have less error message

test: update test

* test: update tests

* chore: remove duplicate translation

* chore: lint for console

* chore: remove comments

* chore: make sure the affect url frame refresh after the language selection change

* chore: add whole_course_translation and language to courseware meta (#1305)

* feat: Add feedback widget UI mock

Add unit tests

Fix snapshot

Clean Sequence component logEvent calls

Clean unit test

Put feedback widget behind whole course translation flag

Fix useFeedbackWidget test

* chore: add src and dest translation

* feat: first iteration of plugin translation

chore: update plugin instruction

* feat: Connect FeedbackWidget with backend services (#1325)

Connect FeedbackWidget with backend services

Move feedback widget to unit translation plugin

* feat: Add authentication to WCT feedback endpoints (#1329)

* chore: add fetch config and move feedback widget for the plugin

chore: rewrite and test the api request

chore: rebase

chore: update translation feedback

chore: test

chore: add more tests

* chore: rebase

* chore: update requested change

* chore: update package

* chore: upgrade frontend-lib-special-exams and frontend-lib-learning-assistant

* chore: update tests

* chore: remove unneeded package

* chore: update example config

* chore: add source-map-loader

* fix: feedback widget render error after submit feedback (#1335)

* fix: feedback widget render error after submit feedback

* fix: widget logic

---------

Co-authored-by: Rodrigo Martin <rodrigom_94@hotmail.com>
2024-03-27 13:39:36 -04:00
Isaac Lee
3917262492 fix: bump exams ver to fix exam timer (#1331) 2024-03-21 15:57:22 -04:00
Alison Langston
7ad120c4d5 feat: upgrade learning assistant version (#1327) 2024-03-18 15:33:38 -04:00
Samir Sabri
94d20833aa feat!: remove Transifex calls for OEP-58 2024-03-18 15:01:23 -04:00
Isaac Lee
4698b11ab9 fix: rollback exams lib to unblock pipeline (#1324) 2024-03-14 14:31:40 -04:00
Awais Ansari
e46d4cc54d feat: add tracking event in upgrade notifications tray (#1323) 2024-03-14 20:52:19 +05:00
Ihor Romaniuk
92d8f637c0 fix: sequence container width and responsive for sequence navigation block (#1219) 2024-03-13 13:29:34 -03:00
Eugene Dyudyunov
c3b786c771 fix: correct rtl for handouts (#1289) 2024-03-13 12:58:56 -03:00
Isaac Lee
00aab61551 chore: update exams lib (#1321) 2024-03-13 10:14:15 -04:00
Ihor Romaniuk
b547ada1a5 fix: RTL for the upgrade notification list (#1220) 2024-03-11 12:31:36 -04:00
Ihor Romaniuk
b7159590d3 fix: wrong text-color class and text contrast on dates page (#1223) 2024-03-11 12:31:27 -04:00
Marcos Rigoli
a2d7f704a6 fix: Updated search modal to match Bootstrap's z-index. (#1318) 2024-03-11 11:44:42 -03:00
Maria Grimaldi
bca3aaccf5 feat: use navigation sequence metadata to disable navigation components (#1273)
Use navigation_disabled sequence metadata based on Hide From TOC
block field, so the student cannot navigate to another sequences in
the course outline.
https://openedx.atlassian.net/wiki/spaces/OEPM/pages/3853975595/Feature+Enhancement+Proposal+Hide+Sections+from+course+outline
2024-03-08 10:51:31 -03:00
Marcos Rigoli
3b46df6d03 fix: Updated exams UI to fix timer (#1317) 2024-03-07 15:48:20 -03:00
Bryann Valderrama
7c8e26d213 feat: show hide from toc
Show message in course outline when Hide From TOC is enabled
https://openedx.atlassian.net/wiki/spaces/OEPM/pages/3853975595/Feature+Enhancement+Proposal+Hide+Sections+from+course+outline
2024-03-06 09:42:19 -03:00
Jeremy Ristau
b1c2c1f7b3 chore: create catalog-info.yaml (#1315)
* chore: create catalog-info.yaml

* chore: update catalog-info.yaml
2024-03-05 09:25:08 -05:00
Emad Rad
4797425a21 docs: broken readme fixed (#1264) 2024-03-03 07:50:01 -05:00
Alison Langston
54bb72b60c feat: update chat visibility (#1310) 2024-03-01 08:17:27 -05:00
renovate[bot]
a91ef116f1 fix(deps): update dependency @edx/frontend-platform to v5.6.1 2024-02-29 00:51:21 +00:00
renovate[bot]
80634c6188 fix(deps): update dependency @edx/frontend-component-header to v4.11.1 2024-02-28 21:46:24 +00:00
renovate[bot]
ec1bad25a7 fix(deps): update dependency @edx/frontend-component-footer to v12.7.1 2024-02-28 21:24:08 +00:00
renovate[bot]
b2980f02a0 fix(deps): update dependency @edx/react-unit-test-utils to v1.7.1 2024-02-28 19:24:33 +00:00
renovate[bot]
9d38e9dc93 chore(deps): update dependency @openedx/frontend-build to v13.0.28 2024-02-28 16:52:17 +00:00
Mashal Malik
c28991d6e0 refactor: replace @edx/paragon and @edx/frontend-build (#1261)
Co-authored-by: mashal-m <mashal.malik@arbisoft.com>
Co-authored-by: Bilal Qamar <59555732+BilalQamar95@users.noreply.github.com>
2024-02-28 13:42:44 -03:00
Alison Langston
ff7366d42a feat: refactor date visibility (#1298) 2024-02-22 15:25:37 -05:00
Alison Langston
6def66677a feat: update chat visibility (#1297) 2024-02-22 11:46:48 -05:00
Alison Langston
8335dec0de feat: upgrade learning assistant version (#1295) 2024-02-21 09:31:08 -05:00
Varsha Menon
992ab4887b feat: show chat for desktop only (#1286) 2024-02-20 14:37:57 -05:00
Alison Langston
5d2aa5e60a feat: update learning assistant version (#1294) 2024-02-20 13:59:48 -05:00
sundasnoreen12
9dd79ca77e fix: fixed empty section issue (#1292)
* fix: fixed empty section issue

* refactor: added checks to hide view
2024-02-15 15:56:04 +05:00
Muhammad Adeel Tajamul
9bfa0d06b6 feat: stick discussion sidebar to top (#1290) 2024-02-15 06:43:32 +05:00
Alison Langston
c6d4bb3b45 feat: update exams and learning-assistant versions (#1291) 2024-02-13 10:29:49 -05:00
Alison Langston
db1fc7782c feat: gate visibility of chat component by active attempt (#1282) 2024-02-12 09:17:32 -05:00
Marcos Rigoli
efd57c326e CoursewareSearch - Show filters if needed only (#1287)
* fix: CoursewareSearch - Show filters if needed only

* fix: Refactored CoursewareSearch filtering logic

* chore: Fixed unit tests for CoursewareResultsFilter

* chore: Added CoursewareSearch hooks coverage
2024-02-09 12:50:42 -03:00
Muhammad Abdullah Waheed
a18ed8ed6c feat: babel-plugin-react-intl to babel-plugin-formatjs migration (#1200) 2024-02-08 14:58:48 -05:00
mashal-m
a340381738 refactor: update lock file version 2024-02-08 14:10:30 -05:00
Michael Roytman
170cbe1da0 Refactor use of frontend-lib-special-exams public API to use hooks. (#1284)
* feat: refactor use of frontend-lib-special-exams public API to hooks

This commit refactors the use of the frontend-lib-special-exams public API to use hooks.

This commit also imports the root reducer from the frontend-lib-special-exams library. This root reducer is used for the specialExams slice when configuring the store for this application.

* feat: update special exams version

* feat: update snapshots

---------

Co-authored-by: Alie Langston <alangsto@wellesley.edu>
2024-02-08 13:58:19 -05:00
Michael Roytman
174de4bc1b feat: update version of frontend-lib-learning-assistant to 1.21.0 (#1285)
This commit installs version 1.21.0 of @edx/frontend-lib-learning-assistant. This commit also refactors the Chat tests to assert on whether the Xpert component is rendered by the Chat component, not whether Xpert actually renders. This is because Xpert now has its own logic to determine whether to render.

This release uses a new GET endpoint published on the Learning Assistant backend to determine whether the Learning Assistant feature is enabled. If the features is not enabled, the Learning Assistant is not rendered, and vice-versa.
2024-02-07 15:39:23 -05:00
alangsto
55e2332b00 feat: remove audit access to learning assistant chat (#1280)
* feat: remove audit access to la

* feat: remove audit access to la
2024-01-29 17:08:17 -05:00
Omar Al-Ithawi
25d746b7c8 feat: tutor-mfe compatiblilty for atlas pull (#1279)
- install atlas
 - remove `--filter` to pull all languages by default
 - use ATLAS_OPTIONS to allow custom `--filter`
 - include frontend-lib-special-exams in `atlas pull`
2024-01-29 12:40:36 -05:00
alangsto
e790e2636f feat: upgrade learning assistant to latest version (#1278) 2024-01-24 15:08:30 -05:00
alangsto
c428222125 Revert "feat: update learning-assistant version" (#1277) 2024-01-24 10:17:21 -05:00
alangsto
58365ba18e feat: update learning-assistant version (#1276) 2024-01-24 09:33:38 -05:00
Troy Sankey
ab87167052 fix: ignore failed outline API call when learner has no access
If the learner doesn't even have access to the course (e.g. because the
course starts in the future), don't worry about a 404 fetching the
course outline since we're not planning to use it anyway.

ENT-8078
2024-01-22 12:49:08 -08:00
sundasnoreen12
4928f505bd test: added test cases for new sidebar (#1267)
* test: added test cases for new sidebar

* test: added factory for verified user

* refactor: updated description for notification widget
2024-01-11 15:54:25 +05:00
alangsto
59a68afa5d feat: gate chat visibility by course end date (#1270) 2024-01-10 09:10:15 -05:00
268 changed files with 11143 additions and 17805 deletions

2
.env
View File

@@ -4,6 +4,7 @@
NODE_ENV='production'
ACCESS_TOKEN_COOKIE_NAME=''
APP_ID='learning'
BASE_URL=''
CONTACT_URL=''
CREDENTIALS_BASE_URL=''
@@ -13,7 +14,6 @@ DISCOVERY_API_BASE_URL=''
DISCUSSIONS_MFE_BASE_URL=''
ECOMMERCE_BASE_URL=''
ENABLE_JUMPNAV='true'
ENABLE_NEW_SIDEBAR=''
ENABLE_NOTICES=''
ENTERPRISE_LEARNER_PORTAL_HOSTNAME=''
EXAMS_BASE_URL=''

View File

@@ -4,6 +4,7 @@
NODE_ENV='development'
ACCESS_TOKEN_COOKIE_NAME='edx-jwt-cookie-header-payload'
APP_ID='learning'
BASE_URL='http://localhost:2000'
CONTACT_URL='http://localhost:18000/contact'
CREDENTIALS_BASE_URL='http://localhost:18150'
@@ -13,7 +14,6 @@ DISCOVERY_API_BASE_URL='http://localhost:18381'
DISCUSSIONS_MFE_BASE_URL='http://localhost:2002'
ECOMMERCE_BASE_URL='http://localhost:18130'
ENABLE_JUMPNAV='true'
ENABLE_NEW_SIDEBAR=''
ENABLE_NOTICES=''
ENTERPRISE_LEARNER_PORTAL_HOSTNAME='localhost:8734'
EXAMS_BASE_URL=''

View File

@@ -4,6 +4,7 @@
NODE_ENV='test'
ACCESS_TOKEN_COOKIE_NAME='edx-jwt-cookie-header-payload'
APP_ID='learning'
BASE_URL='http://localhost:2000'
CONTACT_URL='http://localhost:18000/contact'
CREDENTIALS_BASE_URL='http://localhost:18150'
@@ -13,7 +14,6 @@ DISCOVERY_API_BASE_URL='http://localhost:18381'
DISCUSSIONS_MFE_BASE_URL='http://localhost:2002'
ECOMMERCE_BASE_URL='http://localhost:18130'
ENABLE_JUMPNAV='true'
ENABLE_NEW_SIDEBAR=''
ENABLE_NOTICES=''
ENTERPRISE_LEARNER_PORTAL_HOSTNAME='localhost:8734'
EXAMS_BASE_URL='http://localhost:18740'

View File

@@ -3,3 +3,5 @@ dist/
packages/
node_modules/
jest.config.js
env.config.jsx
example.env.config.jsx

View File

@@ -1,5 +1,5 @@
// eslint-disable-next-line import/no-extraneous-dependencies
const { createConfig } = require('@edx/frontend-build');
const { createConfig } = require('@openedx/frontend-build');
const config = createConfig('eslint', {
rules: {
@@ -12,6 +12,13 @@ const config = createConfig('eslint', {
'react/no-unknown-property': 'off',
'func-names': 'off',
},
settings: {
'import/resolver': {
webpack: {
config: 'webpack.prod.config.js',
},
},
},
});
module.exports = config;

7
.github/dependabot.yml vendored Normal file
View File

@@ -0,0 +1,7 @@
version: 2
updates:
# Adding new check for github-actions
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "weekly"

View File

@@ -9,15 +9,35 @@ on:
jobs:
tests:
runs-on: ubuntu-latest
strategy:
matrix:
node: [18, 20]
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node }}
- run: make validate.ci
- name: Archive code coverage results
uses: actions/upload-artifact@v4
with:
name: code-coverage-report-${{ matrix.node }}
# When we're only using Node 20, replace the line above with the following:
# name: code-coverage-report
path: coverage/*.*
coverage:
runs-on: ubuntu-latest
needs: tests
steps:
- uses: actions/checkout@v3
- name: Setup Nodejs Env
run: echo "NODE_VER=`cat .nvmrc`" >> $GITHUB_ENV
- uses: actions/setup-node@v3
- name: Download code coverage results
uses: actions/download-artifact@v4
with:
node-version: ${{ env.NODE_VER }}
- run: make validate.ci
name: code-coverage-report-20
# When we're only using Node 20, replace the line above with the following:
# name: code-coverage-report
- name: Upload coverage
uses: codecov/codecov-action@v3
uses: codecov/codecov-action@v4
with:
fail_ci_if_error: true
token: ${{ secrets.CODECOV_TOKEN }}

5
.gitignore vendored
View File

@@ -6,6 +6,7 @@
node_modules
npm-debug.log
coverage
env.config.*
dist/
src/i18n/transifex_input.json
@@ -25,3 +26,7 @@ module.config.js
# Local environment overrides
.env.private
src/i18n/messages/
env.config.jsx

2
.nvmrc
View File

@@ -1 +1 @@
18
20

View File

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

View File

@@ -1,20 +1,17 @@
export TRANSIFEX_RESOURCE=frontend-app-learning
transifex_langs = "ar,fr,es_419,zh_CN,pt,it,de,uk,ru,hi,fa_IR,fr_CA,it_IT,pt_PT,de_DE"
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
# This directory must match .babelrc .
transifex_temp = ./temp/babel-plugin-react-intl
transifex_temp = ./temp/babel-plugin-formatjs
precommit:
npm run lint
npm audit
requirements:
npm install
npm ci
i18n.extract:
# Pulling display strings from .jsx files into .json files...
@@ -32,35 +29,21 @@ detect_changed_source_translations:
# Checking for changed translations...
git diff --exit-code $(i18n)
# Pushes translations to Transifex. You must run make extract_translations first.
push_translations:
# Pushing strings to Transifex...
tx push -s
# Fetching hashes from Transifex...
./node_modules/@edx/reactifex/bash_scripts/get_hashed_strings_v3.sh
# Writing out comments to file...
$(transifex_utils) $(transifex_temp) --comments --v3-scripts-path
# Pushing comments to Transifex...
./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 --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/paragon/src/i18n/messages:paragon \
translations/frontend-component-header/src/i18n/messages:frontend-component-header \
translations/frontend-component-footer/src/i18n/messages:frontend-component-footer \
translations/frontend-app-learning/src/i18n/messages:frontend-app-learning
&& atlas pull $(ATLAS_OPTIONS) \
translations/frontend-platform/src/i18n/messages:frontend-platform \
translations/paragon/src/i18n/messages:paragon \
translations/frontend-component-header/src/i18n/messages:frontend-component-header \
translations/frontend-component-footer/src/i18n/messages:frontend-component-footer \
translations/frontend-lib-special-exams/src/i18n/messages:frontend-lib-special-exams \
translations/frontend-app-learning/src/i18n/messages:frontend-app-learning
$(intl_imports) frontend-platform paragon frontend-component-header frontend-component-footer frontend-lib-special-exams frontend-app-learning
$(intl_imports) paragon frontend-component-header frontend-component-footer frontend-app-learning
endif
# This target is used by Travis.
validate-no-uncommitted-package-lock-changes:
@@ -74,6 +57,7 @@ validate:
npm run lint -- --max-warnings 0
npm run test
npm run build
npm run bundlewatch
.PHONY: validate.ci
validate.ci:

View File

@@ -1,77 +1,110 @@
#####################
frontend-app-learning
######################
#####################
|codecov| |license|
********
Purpose
********
*******
This is the Learning MFE (micro-frontend application), which renders all
learner-facing course pages (like the course outline, the progress page,
actual course content, etc).
Please tag **@edx/engage-squad** on any PRs or issues. Thanks.
.. |codecov| image:: https://codecov.io/gh/edx/frontend-app-learning/branch/master/graph/badge.svg?token=3z7XvuzTq3
:target: https://codecov.io/gh/edx/frontend-app-learning
.. |license| image:: https://img.shields.io/badge/license-AGPL-informational
:target: https://github.com/openedx/frontend-app-account/blob/master/LICENSE
***************
Getting Started
***************
Prerequisites
=============
The `devstack`_ is currently recommended as a development environment for your
new MFE. If you start it with ``make dev.up.lms`` that should give you
everything you need as a companion to this frontend.
Note that it is also possible to use `Tutor`_ to develop an MFE. You can refer
to the `relevant tutor-mfe documentation`_ to get started using it.
.. _Devstack: https://github.com/openedx/devstack
`Tutor`_ is currently recommended as a development environment for the Learning
MFE. Most likely, it already has this MFE configured; however, you'll need to
make some changes in order to run it in development mode. You can refer
to the `relevant tutor-mfe documentation`_ for details, or follow the quick
guide below.
.. _Tutor: https://github.com/overhangio/tutor
.. _relevant tutor-mfe documentation: https://github.com/overhangio/tutor-mfe#mfe-development
To use this application, `devstack <https://github.com/openedx/devstack>`__ must be running and you must be logged into it.
- Visit http://localhost:2000/course/course-v1:edX+DemoX+Demo_Course to view the demo course. You can replace ``course-v1:edX+DemoX+Demo_Course`` with a different course key.
Cloning and Setup
=================
Cloning and Startup
===================
1. Clone your new repo:
.. code-block::
.. code-block:: bash
1. Clone your new repo:
git clone https://github.com/openedx/frontend-app-learning.git
``git clone https://github.com/openedx/frontend-app-learning.git``
2. Use node v20.x.
2. Use node v18.x.
The current version of the micro-frontend build scripts supports node 18.
Using other major versions of node *may* work, but this is unsupported. For
convenience, this repository includes an ``.nvmrc`` file to help in setting the
correct node version via `nvm <https://github.com/nvm-sh/nvm>`_.
The current version of the micro-frontend build scripts support node 18.
Using other major versions of node *may* work, but this is unsupported. For
convenience, this repository includes an .nvmrc file to help in setting the
correct node version via `nvm <https://github.com/nvm-sh/nvm>`_.
3. Stop the Tutor devstack, if it's running: ``tutor dev stop``
3. Install npm dependencies:
4. Next, we need to tell Tutor that we're going to be running this repo in
development mode, and it should be excluded from the ``mfe`` container that
otherwise runs every MFE. Run this:
``cd frontend-app-learning && npm ci``
.. code-block:: bash
4. Start the dev server:
tutor mounts add /path/to/frontend-app-learning
``npm start``
5. Start Tutor in development mode. This command will start the LMS and Studio,
and other required MFEs like ``authn`` and ``account``, but will not start
the learning MFE, which we're going to run on the host instead of in a
container managed by Tutor. Run:
.. code-block:: bash
tutor dev start lms cms mfe
Startup
=======
1. Install npm dependencies:
.. code-block:: bash
cd frontend-app-learning && npm ci
2. Start the dev server:
.. code-block:: bash
npm run dev
Then you can access the app at http://local.openedx.io:2000/learning/
Troubleshooting
---------------
If you see an "Invalid Host header" error, then you're probably using a different domain name for your devstack such as
``local.edly.io`` or ``local.overhang.io`` (not the new recommended default, ``local.openedx.io``). In that case, run
these commands to update your devstack's domain names:
.. code-block:: bash
tutor dev stop
tutor config save --set LMS_HOST=local.openedx.io --set CMS_HOST=studio.local.openedx.io
tutor dev launch -I --skip-build
tutor dev stop learning # We will run this MFE on the host
Local module development
=========================
To develop locally on modules that are installed into this app, you'll need to create a ``module.config.js``
file (which is git-ignored) that defines where to find your local modules, for instance::
file (which is git-ignored) that defines where to find your local modules, for instance:
.. code-block:: js
module.exports = {
/*
@@ -84,10 +117,10 @@ file (which is git-ignored) that defines where to find your local modules, for i
may want to use "src" if the module installs React as a peer/dev dependency.
*/
localModules: [
{ moduleName: '@edx/paragon/scss', dir: '../paragon', dist: 'scss' },
{ moduleName: '@edx/paragon', dir: '../paragon', dist: 'dist' },
{ moduleName: '@edx/frontend-enterprise', dir: '../frontend-enterprise', dist: 'src' },
{ moduleName: '@edx/frontend-platform', dir: '../frontend-platform', dist: 'dist' },
{ moduleName: '@openedx/paragon/scss', dir: '../paragon', dist: 'scss' },
{ moduleName: '@openedx/paragon', dir: '../paragon', dist: 'dist' },
{ moduleName: '@openedx/frontend-enterprise', dir: '../frontend-enterprise', dist: 'src' },
{ moduleName: '@openedx/frontend-platform', dir: '../frontend-platform', dist: 'dist' },
],
};
@@ -100,8 +133,14 @@ The Learning MFE is similar to all the other Open edX MFEs. Read the Open
edX Developer Guide's section on
`MFE applications <https://edx.readthedocs.io/projects/edx-developer-docs/en/latest/developers_guide/micro_frontends_in_open_edx.html>`_.
Plugins
=======
This MFE can be customized using `Frontend Plugin Framework <https://github.com/openedx/frontend-plugin-framework>`_.
The parts of this MFE that can be customized in that manner are documented `here </src/plugin-slots>`_.
Environment Variables
======================
=====================
This MFE is configured via environment variables supplied at build time.
All micro-frontends have a shared set of required environment variables,
@@ -127,7 +166,7 @@ SOCIAL_UTM_MILESTONE_CAMPAIGN
SUPPORT_URL_CALCULATOR_MATH
A link that explains how to use the in-course calculator. You can use the
one in the example below, if you don't want to have your own branded version.
one in the example below if you don't want to have your own branded version.
Example: https://support.edx.org/hc/en-us/articles/360000038428-Entering-math-expressions-in-assignments-or-the-calculator
@@ -140,7 +179,7 @@ SUPPORT_URL_ID_VERIFICATION
SUPPORT_URL_VERIFIED_CERTIFICATE
A link that explains what a verified certificate is. You can use the
one in the example below, if you don't want to have your own branded version.
one in the example below if you don't want to have your own branded version.
Optional.
Example: https://support.edx.org/hc/en-us/articles/206502008-What-is-a-verified-certificate
@@ -156,13 +195,13 @@ TWITTER_URL
A link to your Twitter account. The Twitter social-share link won't appear
unless this is set. Optional.
Example: https://twitter.com/edXOnline
Example: https://twitter.com/openedx
Getting Help
===========
============
If you're having trouble, we have discussion forums at
https://discuss.openedx.org where you can connect with others in the community.
If you're having trouble, we have `discussion forums`_
where you can connect with others in the community.
Our real-time conversations are on Slack. You can request a `Slack
invitation`_, then join our `community Slack workspace`_. Because this is a
@@ -180,17 +219,18 @@ For more information about these options, see the `Getting Help`_ page.
.. _community Slack workspace: https://openedx.slack.com/
.. _#wg-frontend channel: https://openedx.slack.com/archives/C04BM6YC7A6
.. _Getting Help: https://openedx.org/community/connect
.. _discussion forums: https://discuss.openedx.org
Contributing
============
Contributions are very welcome. Please read `How To Contribute`_ for details.
Contributions are very welcome. Please read `How To Contribute`_ for details.
.. _How To Contribute: https://openedx.org/r/how-to-contribute
This project is currently accepting all types of contributions, bug fixes,
security fixes, maintenance work, or new features. However, please make sure
to have a discussion about your new feature idea with the maintainers prior to
to discuss your new feature idea with the maintainers before
beginning development to maximize the chances of your change being accepted.
You can start a conversation by creating a new issue on this repo summarizing
your idea.

18
catalog-info.yaml Normal file
View File

@@ -0,0 +1,18 @@
# 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-learning'
description: "This is the Learning MFE, which renders all learner-facing course pages."
links:
- url: "https://github.com/openedx/frontend-app-learning"
title: "Learning MFE"
icon: "Web"
annotations:
openedx.org/arch-interest-groups: ""
spec:
owner: group:committers-frontend-app-learning
type: 'website'
lifecycle: 'production'

24
example.env.config.jsx Normal file
View File

@@ -0,0 +1,24 @@
import UnitTranslationPlugin from '@edx/unit-translation-selector-plugin';
import { PLUGIN_OPERATIONS, DIRECT_PLUGIN } from '@openedx/frontend-plugin-framework';
// Load environment variables from .env file
const config = {
...process.env,
pluginSlots: {
unit_title_plugin: {
plugins: [
{
op: PLUGIN_OPERATIONS.Insert,
widget: {
id: 'unit_title_plugin',
type: DIRECT_PLUGIN,
priority: 1,
RenderWidget: UnitTranslationPlugin,
},
},
],
},
},
};
export default config;

View File

@@ -1,6 +1,6 @@
const { createConfig } = require('@edx/frontend-build');
const { createConfig } = require('@openedx/frontend-build');
module.exports = createConfig('jest', {
const config = createConfig('jest', {
setupFilesAfterEnv: [
'<rootDir>/src/setupTest.js',
],
@@ -9,13 +9,35 @@ module.exports = createConfig('jest', {
'src/i18n',
'src/.*\\.exp\\..*',
],
// see https://github.com/axios/axios/issues/5026
moduleNameMapper: {
"^axios$": "axios/dist/axios.js",
// See https://stackoverflow.com/questions/72382316/jest-encountered-an-unexpected-token-react-markdown
'react-markdown': '<rootDir>/node_modules/react-markdown/react-markdown.min.js',
'@src/(.*)': '<rootDir>/src/$1',
// Explicit mapping to ensure Jest resolves the module correctly
'@edx/frontend-lib-special-exams': '<rootDir>/node_modules/@edx/frontend-lib-special-exams',
},
testTimeout: 30000,
globalSetup: "./global-setup.js",
verbose: true,
testEnvironment: 'jsdom',
globalSetup: "./global-setup.js"
});
// delete config.testURL;
config.reporters = [...(config.reporters || []), ["jest-console-group-reporter", {
// change this setting if need to see less details for each test
// reportType: "summary" | "details",
// enable: true | false,
afterEachTest: {
enable: true,
filePaths: false,
reportType: "details",
},
afterAllTests: {
reportType: "summary",
enable: true,
filePaths: true,
},
}]];
module.exports = config;

14613
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -11,12 +11,15 @@
],
"scripts": {
"build": "fedx-scripts webpack",
"i18n_extract": "BABEL_ENV=i18n fedx-scripts babel src --quiet > /dev/null",
"bundlewatch": "bundlewatch",
"i18n_extract": "fedx-scripts formatjs extract",
"lint": "fedx-scripts eslint --ext .js --ext .jsx .",
"lint:fix": "fedx-scripts eslint --fix --ext .js --ext .jsx .",
"prepare": "husky install",
"postinstall": "patch-package",
"snapshot": "fedx-scripts jest --updateSnapshot",
"start": "fedx-scripts webpack-dev-server --progress",
"dev": "PUBLIC_PATH=/learning/ MFE_CONFIG_API_URL='http://localhost:8000/api/mfe_config/v1' fedx-scripts webpack-dev-server --progress --host apps.local.openedx.io",
"test": "fedx-scripts jest --coverage --passWithNoTests"
},
"author": "edX",
@@ -30,26 +33,33 @@
},
"dependencies": {
"@edx/brand": "npm:@openedx/brand-openedx@^1.2.2",
"@edx/frontend-component-footer": "12.2.1",
"@edx/frontend-component-header": "4.6.0",
"@edx/frontend-lib-special-exams": "2.27.0",
"@edx/frontend-lib-learning-assistant": "^1.19.0",
"@edx/frontend-platform": "5.5.2",
"@edx/paragon": "20.46.0",
"@edx/react-unit-test-utils": "npm:@edx/react-unit-test-utils@1.7.0",
"@fortawesome/fontawesome-svg-core": "1.3.0",
"@edx/browserslist-config": "1.2.0",
"@edx/frontend-component-header": "^5.3.3",
"@edx/frontend-lib-learning-assistant": "^2.2.4",
"@edx/frontend-lib-special-exams": "^3.1.3",
"@edx/frontend-platform": "^8.0.0",
"@edx/openedx-atlas": "^0.6.0",
"@edx/react-unit-test-utils": "3.0.0",
"@fortawesome/free-brands-svg-icons": "5.15.4",
"@fortawesome/free-regular-svg-icons": "5.15.4",
"@fortawesome/free-solid-svg-icons": "5.15.4",
"@fortawesome/react-fontawesome": "^0.1.4",
"@openedx/frontend-build": "14.1.2",
"@openedx/frontend-plugin-framework": "^1.2.1",
"@openedx/frontend-slot-footer": "^1.0.2",
"@openedx/paragon": "^22.3.0",
"@popperjs/core": "2.11.8",
"@reduxjs/toolkit": "1.8.1",
"classnames": "2.3.2",
"core-js": "3.22.2",
"history": "5.3.0",
"buffer": "^6.0.3",
"classnames": "2.5.1",
"copy-webpack-plugin": "^11.0.0",
"husky": "7.0.4",
"joi": "^17.11.0",
"js-cookie": "3.0.5",
"lodash": "^4.17.21",
"lodash.camelcase": "4.3.0",
"patch-package": "^8.0.0",
"postcss-loader": "^8.1.1",
"prop-types": "15.8.1",
"query-string": "^7.1.3",
"react": "17.0.2",
@@ -60,25 +70,34 @@
"react-router-dom": "6.15.0",
"react-share": "4.4.1",
"redux": "4.1.2",
"regenerator-runtime": "0.13.11",
"reselect": "4.1.8",
"truncate-html": "1.0.4",
"util": "0.12.5"
"sass": "^1.79.3",
"sass-loader": "^16.0.2",
"source-map-loader": "^5.0.0",
"truncate-html": "1.1.1"
},
"devDependencies": {
"@edx/browserslist-config": "1.2.0",
"@edx/frontend-build": "^12.9.10",
"@edx/reactifex": "2.2.0",
"@pact-foundation/pact": "^11.0.2",
"@testing-library/jest-dom": "5.16.5",
"@pact-foundation/pact": "^13.0.0",
"@testing-library/jest-dom": "5.17.0",
"@testing-library/react": "12.1.5",
"@testing-library/react-hooks": "^8.0.1",
"@testing-library/user-event": "13.5.0",
"axios-mock-adapter": "1.20.0",
"copy-webpack-plugin": "^11.0.0",
"es-check": "6.2.1",
"husky": "7.0.4",
"jest": "29.5.0",
"axios-mock-adapter": "2.0.0",
"bundlewatch": "^0.4.0",
"eslint-import-resolver-webpack": "^0.13.9",
"jest": "^29.7.0",
"jest-console-group-reporter": "^1.1.1",
"jest-when": "^3.6.0",
"rosie": "2.1.1"
},
"bundlewatch": {
"files": [
{
"path": "dist/*.js",
"maxSize": "1300kB"
}
],
"normalizeFilenames": "^.+?(\\..+?)\\.\\w+$"
}
}

View File

@@ -0,0 +1,36 @@
diff --git a/node_modules/@openedx/frontend-build/config/webpack.prod.config.js b/node_modules/@openedx/frontend-build/config/webpack.prod.config.js
index 2879dd9..9efd0fc 100644
--- a/node_modules/@openedx/frontend-build/config/webpack.prod.config.js
+++ b/node_modules/@openedx/frontend-build/config/webpack.prod.config.js
@@ -12,6 +12,7 @@ const NewRelicSourceMapPlugin = require('@edx/new-relic-source-map-webpack-plugi
const HtmlWebpackPlugin = require('html-webpack-plugin');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const path = require('path');
+const fs = require('fs');
const PostCssAutoprefixerPlugin = require('autoprefixer');
const PostCssRTLCSS = require('postcss-rtlcss');
const PostCssCustomMediaCSS = require('postcss-custom-media');
@@ -23,6 +24,23 @@ const HtmlWebpackNewRelicPlugin = require('../lib/plugins/html-webpack-new-relic
const commonConfig = require('./webpack.common.config');
const presets = require('../lib/presets');
+/**
+ * This condition confirms whether the configuration for the MFE has switched to a JS-based configuration
+ * as previously implemented in frontend-build and frontend-platform. If the environment variable JS_CONFIG_FILEPATH
+ * exists, then an env.config.js(x) file will be copied from the location referenced by the environment variable to the
+ * root directory. Its env variables can be accessed with getConfig().
+ *
+ * https://github.com/openedx/frontend-build/blob/master/docs/0002-js-environment-config.md
+ * https://github.com/openedx/frontend-platform/blob/master/docs/decisions/0007-javascript-file-configuration.rst
+ */
+
+const envConfigPath = process.env.JS_CONFIG_FILEPATH;
+
+if (envConfigPath) {
+ const envConfigFilename = envConfigPath.slice(envConfigPath.indexOf('env.config'));
+ fs.copyFileSync(envConfigPath, envConfigFilename);
+}
+
// Add process env vars. Currently used only for setting the PUBLIC_PATH.
dotenv.config({
path: path.resolve(process.cwd(), '.env'),

View File

@@ -3,8 +3,8 @@ import { sendTrackEvent } from '@edx/frontend-platform/analytics';
import {
FormattedMessage, FormattedDate, injectIntl, intlShape,
} from '@edx/frontend-platform/i18n';
import { Alert, Hyperlink } from '@edx/paragon';
import { Info } from '@edx/paragon/icons';
import { Alert, Hyperlink } from '@openedx/paragon';
import { Info } from '@openedx/paragon/icons';
import messages from './messages';

View File

@@ -1,7 +1,7 @@
import React from 'react';
import PropTypes from 'prop-types';
import { FormattedMessage, FormattedDate } from '@edx/frontend-platform/i18n';
import { PageBanner } from '@edx/paragon';
import { PageBanner } from '@openedx/paragon';
const AccessExpirationMasqueradeBanner = ({ payload }) => {
const {

View File

@@ -1,8 +1,8 @@
import React from 'react';
import { FormattedMessage, injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import PropTypes from 'prop-types';
import { Alert, Hyperlink } from '@edx/paragon';
import { WarningFilled } from '@edx/paragon/icons';
import { Alert, Hyperlink } from '@openedx/paragon';
import { WarningFilled } from '@openedx/paragon/icons';
import { getConfig } from '@edx/frontend-platform';
import genericMessages from './messages';

View File

@@ -6,8 +6,8 @@ import {
FormattedRelativeTime,
FormattedTime,
} from '@edx/frontend-platform/i18n';
import { Alert } from '@edx/paragon';
import { Info } from '@edx/paragon/icons';
import { Alert } from '@openedx/paragon';
import { Info } from '@openedx/paragon/icons';
import { useModel } from '../../generic/model-store';

View File

@@ -1,7 +1,7 @@
import React from 'react';
import PropTypes from 'prop-types';
import { FormattedMessage, FormattedDate } from '@edx/frontend-platform/i18n';
import { PageBanner } from '@edx/paragon';
import { PageBanner } from '@openedx/paragon';
import { useModel } from '../../generic/model-store';

View File

@@ -1,8 +1,8 @@
import React from 'react';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import PropTypes from 'prop-types';
import { Alert, Button } from '@edx/paragon';
import { Info, WarningFilled } from '@edx/paragon/icons';
import { Alert, Button } from '@openedx/paragon';
import { Info, WarningFilled } from '@openedx/paragon/icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faSpinner } from '@fortawesome/free-solid-svg-icons';

View File

@@ -7,8 +7,8 @@ import {
Button,
Spinner,
Icon,
} from '@edx/paragon';
import { Check, ArrowForward } from '@edx/paragon/icons';
} from '@openedx/paragon';
import { Check, ArrowForward } from '@openedx/paragon/icons';
import { FormattedMessage, injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { sendActivationEmail } from '../../courseware/data';
import messages from './messages';

View File

@@ -2,8 +2,8 @@ import React from 'react';
import { getConfig } from '@edx/frontend-platform';
import { injectIntl, intlShape, FormattedMessage } from '@edx/frontend-platform/i18n';
import { getLoginRedirectUrl } from '@edx/frontend-platform/auth';
import { Alert, Hyperlink } from '@edx/paragon';
import { WarningFilled } from '@edx/paragon/icons';
import { Alert, Hyperlink } from '@openedx/paragon';
import { WarningFilled } from '@openedx/paragon/icons';
import genericMessages from '../../generic/messages';

View File

@@ -20,6 +20,7 @@ export const DECODE_ROUTES = {
export const ROUTES = {
UNSUBSCRIBE: '/goal-unsubscribe/:token',
PREFERENCES_UNSUBSCRIBE: '/preferences-unsubscribe/:userToken/:updatePatch',
REDIRECT: '/redirect/*',
DASHBOARD: 'dashboard',
ENTERPRISE_LEARNER_DASHBOARD: 'enterprise-learner-dashboard',
@@ -33,3 +34,24 @@ export const REDIRECT_MODES = {
HOME_REDIRECT: 'home-redirect',
SURVEY_REDIRECT: 'survey-redirect',
};
export const VERIFIED_MODES = [
'professional',
'verified',
'no-id-professional',
'credit',
'masters',
'executive-education',
'paid-executive-education',
'paid-bootcamp',
];
export const WIDGETS = {
DISCUSSIONS: 'DISCUSSIONS',
NOTIFICATIONS: 'NOTIFICATIONS',
};
export const LOADING = 'loading';
export const LOADED = 'loaded';
export const FAILED = 'failed';
export const DENIED = 'denied';

View File

@@ -1,6 +1,6 @@
import React, { useMemo } from 'react';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { Tabs, Tab } from '@edx/paragon';
import { Tabs, Tab } from '@openedx/paragon';
import { useParams } from 'react-router';
import CoursewareSearchResults from './CoursewareSearchResults';
@@ -8,15 +8,10 @@ import messages from './messages';
import { useCoursewareSearchParams } from './hooks';
import { useModel } from '../../generic/model-store';
const allFilterKey = 'all';
const otherFilterKey = 'other';
const allowedFilterKeys = {
[allFilterKey]: true,
text: true,
video: true,
sequence: true,
[otherFilterKey]: true,
};
const filterAll = 'all';
const filterTypes = ['text', 'video', 'sequence'];
const filterOther = 'other';
const validFilters = [filterAll, ...filterTypes, filterOther];
export const CoursewareSearchResultsFilter = ({ intl }) => {
const { courseId } = useParams();
@@ -27,23 +22,38 @@ export const CoursewareSearchResultsFilter = ({ intl }) => {
const { results: data = [] } = lastSearch;
const results = useMemo(() => data.reduce((acc, { type, ...rest }) => {
acc[allFilterKey] = [...(acc[allFilterKey] || []), { type: allFilterKey, ...rest }];
if (type === allFilterKey) { return acc; }
// If there's no data, we show an empty result.
if (!data.length) { return <CoursewareSearchResults />; }
let targetKey = otherFilterKey;
if (allowedFilterKeys[type]) { targetKey = type; }
acc[targetKey] = [...(acc[targetKey] || []), { type: targetKey, ...rest }];
return acc;
}, {}), [data]);
const results = useMemo(() => {
// This reducer distributes the data into different groups to make it easy to
// use on the filters.
// All results are added to the "all" key and then to its proper group key as well.
const grouped = data.reduce((acc, { type, ...rest }) => {
const resultType = filterTypes.includes(type) ? type : filterOther;
acc[filterAll].push({ type: resultType, ...rest });
acc[resultType] = [...(acc[resultType] || []), { type: resultType, ...rest }];
return acc;
}, { [filterAll]: [] });
const filters = useMemo(() => Object.keys(allowedFilterKeys).map((key) => ({
// This is just to format the output object with the expected tab order.
const output = {};
validFilters.forEach(key => { if (grouped[key]) { output[key] = grouped[key]; } });
return output;
}, [lastSearch]);
const tabKeys = Object.keys(results);
// Filter has no use if it has only 2 tabs (The "all" tab and another one with the same items).
if (tabKeys.length < 3) { return <CoursewareSearchResults results={results[filterAll]} />; }
const filters = useMemo(() => tabKeys.map((key) => ({
key,
label: intl.formatMessage(messages[`filter:${key}`]),
count: results[key]?.length || 0,
count: results[key].length,
})), [results]);
const activeKey = allowedFilterKeys[filterKeyword] ? filterKeyword : allFilterKey;
const activeKey = validFilters.includes(filterKeyword) ? filterKeyword : filterAll;
return (
<Tabs
@@ -54,7 +64,7 @@ export const CoursewareSearchResultsFilter = ({ intl }) => {
activeKey={activeKey}
onSelect={setFilter}
>
{filters.map(({ key, label }) => (
{filters.filter(({ count }) => (count > 0)).map(({ key, label }) => (
<Tab key={key} eventKey={key} title={label} data-testid={`courseware-search-results-tabs-${key}`}>
<CoursewareSearchResults results={results[key]} />
</Tab>

View File

@@ -19,6 +19,12 @@ jest.mock('../../generic/model-store', () => ({
useModel: jest.fn(),
}));
global.ResizeObserver = jest.fn().mockImplementation(() => ({
observe: jest.fn(),
unobserve: jest.fn(),
disconnect: jest.fn(),
}));
const decodedCourseId = 'course-v1:edX+DemoX+Demo_Course';
const decodedSequenceId = 'block-v1:edX+DemoX+Demo_Course+type@sequential+block@edx_introduction';
const decodedUnitId = 'block-v1:edX+DemoX+Demo_Course+type@vertical+block@vertical_0270f6de40fc';
@@ -52,49 +58,75 @@ function renderComponent(props = {}) {
describe('CoursewareSearchResultsFilter', () => {
beforeAll(initializeMockApp);
describe('</CoursewareSearchResultsFilter />', () => {
beforeEach(() => {
useCoursewareSearchParams.mockReturnValue(coursewareSearch);
});
beforeEach(() => {
useCoursewareSearchParams.mockReturnValue(coursewareSearch);
});
afterEach(() => {
jest.clearAllMocks();
afterEach(() => {
jest.clearAllMocks();
});
describe('when returning full results', () => {
beforeEach(() => {
useModel.mockReturnValue(searchResultsFactory());
renderComponent();
});
it('should render without errors', async () => {
useModel.mockReturnValue(searchResultsFactory());
await renderComponent();
await waitFor(() => {
expect(screen.queryByTestId('courseware-search-results-tabs-all')).toBeInTheDocument();
expect(screen.queryByTestId('courseware-search-results-tabs-text')).toBeInTheDocument();
expect(screen.queryByTestId('courseware-search-results-tabs-video')).toBeInTheDocument();
expect(screen.queryByTestId('courseware-search-results-tabs-sequence')).toBeInTheDocument();
expect(screen.queryByTestId('courseware-search-results-tabs-other')).toBeInTheDocument();
expect(useCoursewareSearchParams).toBeCalled();
});
expect(screen.queryByTestId('courseware-search-results-tabs')).toBeInTheDocument();
expect(screen.queryByTestId('courseware-search-results-tabs-all')).toBeInTheDocument();
expect(screen.queryByTestId('courseware-search-results-tabs-text')).toBeInTheDocument();
expect(screen.queryByTestId('courseware-search-results-tabs-video')).toBeInTheDocument();
expect(screen.queryByTestId('courseware-search-results-tabs-sequence')).toBeInTheDocument();
expect(screen.queryByTestId('courseware-search-results-tabs-other')).toBeInTheDocument();
});
});
describe('when returning only one result type', () => {
beforeEach(async () => {
// Get results for only videos
const data = searchResultsFactory();
const onlyVideos = data.results.filter(({ type }) => type === 'video');
const filteredResults = {
...data,
results: onlyVideos,
};
useModel.mockReturnValue(filteredResults);
await renderComponent();
});
describe('when there are not results', () => {
it('should render without errors', async () => {
useModel.mockReturnValue(searchResultsFactory('blah', {
results: [],
filters: [],
total: 0,
maxScore: null,
ms: 5,
}));
await renderComponent();
await waitFor(() => {
expect(screen.queryByTestId('courseware-search-results-tabs-all')).toBeInTheDocument();
expect(screen.queryByTestId('courseware-search-results-tabs-text')).toBeInTheDocument();
expect(screen.queryByTestId('courseware-search-results-tabs-video')).toBeInTheDocument();
expect(screen.queryByTestId('courseware-search-results-tabs-sequence')).toBeInTheDocument();
expect(screen.queryByTestId('courseware-search-results-tabs-other')).toBeInTheDocument();
});
it('should not render', async () => {
await waitFor(() => {
expect(useCoursewareSearchParams).toBeCalled();
});
expect(screen.queryByTestId('courseware-search-results-tabs')).not.toBeInTheDocument();
});
});
describe('when there are not results', () => {
beforeEach(async () => {
useModel.mockReturnValue(searchResultsFactory('blah', {
results: [],
filters: [],
total: 0,
maxScore: null,
ms: 5,
}));
await renderComponent();
});
it('should not render', async () => {
await waitFor(() => {
expect(useCoursewareSearchParams).toBeCalled();
});
expect(screen.queryByTestId('courseware-search-results-tabs')).not.toBeInTheDocument();
});
});
});

View File

@@ -5,10 +5,10 @@ import { sendTrackingLogEvent } from '@edx/frontend-platform/analytics';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import {
Alert, Button, Icon, Spinner,
} from '@edx/paragon';
} from '@openedx/paragon';
import {
Close,
} from '@edx/paragon/icons';
} from '@openedx/paragon/icons';
import { setShowSearch } from '../data/slice';
import { useCoursewareSearchParams, useElementBoundingBox, useLockScroll } from './hooks';
import messages from './messages';
@@ -103,7 +103,7 @@ const CoursewareSearch = ({ intl, ...sectionProps }) => {
</div>
<div className="courseware-search__outer-content">
<div className="courseware-search__content">
<h1 class="h2">{intl.formatMessage(messages.searchModuleTitle)}</h1>
<h1 className="h2">{intl.formatMessage(messages.searchModuleTitle)}</h1>
<CoursewareSearchForm
searchTerm={searchKeyword}
onSubmit={handleSubmit}
@@ -122,16 +122,16 @@ const CoursewareSearch = ({ intl, ...sectionProps }) => {
)}
{status === 'results' ? (
<>
<div
className="courseware-search__results-summary"
aria-live="polite"
aria-relevant="all"
aria-atomic="true"
data-testid="courseware-search-summary"
>{total > 0
? intl.formatMessage(messages.searchResultsLabel, { total, keyword: lastSearchKeyword })
: intl.formatMessage(messages.searchResultsNone)}
</div>
{total > 0 ? (
<div
className="courseware-search__results-summary"
aria-live="polite"
aria-relevant="all"
aria-atomic="true"
data-testid="courseware-search-summary"
>{intl.formatMessage(messages.searchResultsLabel, { total, keyword: lastSearchKeyword })}
</div>
) : null}
<CoursewareSearchResultsFilterContainer />
</>
) : null}

View File

@@ -236,14 +236,14 @@ describe('CoursewareSearch', () => {
expect(screen.queryByTestId('courseware-search-error')).toBeInTheDocument();
});
it('should show "No results found." if results is empty', () => {
it('should not show a summary if there are no results', () => {
mockModels({
searchKeyword: 'test',
total: 0,
});
renderComponent();
expect(screen.queryByTestId('courseware-search-summary').textContent).toBe('No results found.');
expect(screen.queryByTestId('courseware-search-summary')).not.toBeInTheDocument();
});
it('should show a summary for the results', () => {

View File

@@ -1,6 +1,6 @@
import React from 'react';
import PropTypes from 'prop-types';
import { SearchField } from '@edx/paragon';
import { SearchField } from '@openedx/paragon';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import messages from './messages';

View File

@@ -1,16 +1,17 @@
import React from 'react';
import {
Folder, TextFields, VideoCamera, Article,
} from '@edx/paragon/icons';
} from '@openedx/paragon/icons';
import { getConfig } from '@edx/frontend-platform';
import { Icon } from '@edx/paragon';
import { Icon } from '@openedx/paragon';
import PropTypes from 'prop-types';
import CoursewareSearchEmpty from './CoursewareSearchEmpty';
const iconTypeMapping = {
document: Folder,
text: TextFields,
video: VideoCamera,
sequence: Folder,
other: Article,
};
const defaultIcon = Article;
@@ -34,9 +35,7 @@ const CoursewareSearchResults = ({ results = [] }) => {
}) => {
const key = type.toLowerCase();
const icon = iconTypeMapping[key] || defaultIcon;
const isExternal = !url.startsWith('/');
const linkProps = isExternal ? {
href: url,
target: '_blank',

View File

@@ -1,7 +1,7 @@
import React, { useEffect } from 'react';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { Button, Icon } from '@edx/paragon';
import { Search } from '@edx/paragon/icons';
import { Button } from '@openedx/paragon';
import { ManageSearch } from '@openedx/paragon/icons';
import { useDispatch } from 'react-redux';
import messages from './messages';
import { useCoursewareSearchFeatureFlag, useCoursewareSearchParams } from './hooks';
@@ -25,16 +25,17 @@ const CoursewareSearchToggle = ({
if (!enabled) { return null; }
return (
<div className="courseware-searc-toggle">
<div className="courseware-search-toggle">
<Button
variant="tertiary"
variant="outline-primary"
size="sm"
className="p-1 mt-2 mr-2 rounded-lg"
className="p-1 mt-2 mr-2"
aria-label={intl.formatMessage(messages.searchOpenAction)}
onClick={handleSearchOpenClick}
data-testid="courseware-search-open-button"
iconAfter={ManageSearch}
>
<Icon src={Search} />
{intl.formatMessage(messages.contentSearchButton)}
</Button>
</div>
);

View File

@@ -26,7 +26,7 @@ exports[`CoursewareSearchResults when list of results is provided should match t
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M3 3v18h18V3H3Zm11 14H7v-2h7v2Zm3-4H7v-2h10v2Zm0-4H7V7h10v2Z"
d="M10 4H2v16h20V6H12l-2-2z"
fill="currentColor"
/>
</svg>
@@ -140,7 +140,7 @@ exports[`CoursewareSearchResults when list of results is provided should match t
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M3 3v18h18V3H3Zm11 14H7v-2h7v2Zm3-4H7v-2h10v2Zm0-4H7V7h10v2Z"
d="M10 4H2v16h20V6H12l-2-2z"
fill="currentColor"
/>
</svg>

View File

@@ -1,29 +1,29 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`mapSearchResponse when the response is correct should match snapshot 1`] = `
Object {
"filters": Array [
Object {
{
"filters": [
{
"count": 7,
"key": "capa",
"label": "CAPA",
},
Object {
{
"count": 2,
"key": "sequence",
"label": "Sequence",
},
Object {
{
"count": 9,
"key": "text",
"label": "Text",
},
Object {
{
"count": 1,
"key": "unknown",
"label": "Unknown",
},
Object {
{
"count": 2,
"key": "video",
"label": "Video",
@@ -31,11 +31,11 @@ Object {
],
"maxScore": 3.4545178,
"ms": 5,
"results": Array [
Object {
"results": [
{
"contentHits": 0,
"id": "block-v1:edX+DemoX+Demo_Course+type@sequential+block@edx_introduction",
"location": Array [
"location": [
"Introduction",
"Demo Course Overview",
],
@@ -44,10 +44,10 @@ Object {
"type": "sequence",
"url": "/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@sequential+block@edx_introduction",
},
Object {
{
"contentHits": 0,
"id": "block-v1:edX+DemoX+Demo_Course+type@html+block@5e009378f0b64585baa0a14b155974b9",
"location": Array [
"location": [
"About Exams and Certificates",
"edX Exams",
"Passing a Course",
@@ -57,10 +57,10 @@ Object {
"type": "text",
"url": "/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@html+block@5e009378f0b64585baa0a14b155974b9",
},
Object {
{
"contentHits": 0,
"id": "block-v1:edX+DemoX+Demo_Course+type@vertical+block@c7e98fd39a6944edb6b286c32e1150ff",
"location": Array [
"location": [
"About Exams and Certificates",
"edX Exams",
"Passing a Course",
@@ -70,10 +70,10 @@ Object {
"type": "sequence",
"url": "/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@vertical+block@c7e98fd39a6944edb6b286c32e1150ff",
},
Object {
{
"contentHits": 0,
"id": "block-v1:edX+DemoX+Demo_Course+type@problem+block@0d759dee4f9d459c8956136dbde55f02",
"location": Array [
"location": [
"Example Week 1: Getting Started",
"Homework - Question Styles",
"Text input",
@@ -83,10 +83,10 @@ Object {
"type": "capa",
"url": "/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@problem+block@0d759dee4f9d459c8956136dbde55f02",
},
Object {
{
"contentHits": 0,
"id": "block-v1:edX+DemoX+Demo_Course+type@problem+block@c554538a57664fac80783b99d9d6da7c",
"location": Array [
"location": [
"Example Week 1: Getting Started",
"Homework - Question Styles",
"Pointing on a Picture",
@@ -96,10 +96,10 @@ Object {
"type": "capa",
"url": "/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@problem+block@c554538a57664fac80783b99d9d6da7c",
},
Object {
{
"contentHits": 0,
"id": "block-v1:edX+DemoX+Demo_Course+type@problem+block@45d46192272c4f6db6b63586520bbdf4",
"location": Array [
"location": [
"About Exams and Certificates",
"edX Exams",
"Getting Answers",
@@ -109,10 +109,10 @@ Object {
"type": "capa",
"url": "/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@problem+block@45d46192272c4f6db6b63586520bbdf4",
},
Object {
{
"contentHits": 0,
"id": "block-v1:edX+DemoX+Demo_Course+type@video+block@0b9e39477cf34507a7a48f74be381fdd",
"location": Array [
"location": [
"Introduction",
"Demo Course Overview",
"Introduction: Video and Sequences",
@@ -122,10 +122,10 @@ Object {
"type": "video",
"url": "/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@video+block@0b9e39477cf34507a7a48f74be381fdd",
},
Object {
{
"contentHits": 0,
"id": "block-v1:edX+DemoX+Demo_Course+type@problem+block@a0effb954cca4759994f1ac9e9434bf4",
"location": Array [
"location": [
"Example Week 1: Getting Started",
"Homework - Question Styles",
"Multiple Choice Questions",
@@ -135,10 +135,10 @@ Object {
"type": "capa",
"url": "/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@problem+block@a0effb954cca4759994f1ac9e9434bf4",
},
Object {
{
"contentHits": 0,
"id": "block-v1:edX+DemoX+Demo_Course+type@problem+block@75f9562c77bc4858b61f907bb810d974",
"location": Array [
"location": [
"Example Week 1: Getting Started",
"Homework - Question Styles",
"Numerical Input",
@@ -148,10 +148,10 @@ Object {
"type": "capa",
"url": "/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@problem+block@75f9562c77bc4858b61f907bb810d974",
},
Object {
{
"contentHits": 0,
"id": "block-v1:edX+DemoX+Demo_Course+type@video+block@636541acbae448d98ab484b028c9a7f6",
"location": Array [
"location": [
"Example Week 1: Getting Started",
"Lesson 1 - Getting Started",
"Video Presentation Styles",
@@ -161,10 +161,10 @@ Object {
"type": "video",
"url": "/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@video+block@636541acbae448d98ab484b028c9a7f6",
},
Object {
{
"contentHits": 0,
"id": "block-v1:edX+DemoX+Demo_Course+type@problem+block@python_grader",
"location": Array [
"location": [
"Example Week 2: Get Interactive",
"Homework - Labs and Demos",
"Code Grader",
@@ -174,10 +174,10 @@ Object {
"type": "capa",
"url": "/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@problem+block@python_grader",
},
Object {
{
"contentHits": 0,
"id": "block-v1:edX+DemoX+Demo_Course+type@problem+block@9cee77a606ea4c1aa5440e0ea5d0f618",
"location": Array [
"location": [
"Example Week 1: Getting Started",
"Lesson 1 - Getting Started",
"Interactive Questions",
@@ -187,10 +187,10 @@ Object {
"type": "capa",
"url": "/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@problem+block@9cee77a606ea4c1aa5440e0ea5d0f618",
},
Object {
{
"contentHits": 0,
"id": "block-v1:edX+DemoX+Demo_Course+type@html+block@030e35c4756a4ddc8d40b95fbbfff4d4",
"location": Array [
"location": [
"Introduction",
"Demo Course Overview",
"Introduction: Video and Sequences",
@@ -200,10 +200,10 @@ Object {
"type": "text",
"url": "/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@html+block@030e35c4756a4ddc8d40b95fbbfff4d4",
},
Object {
{
"contentHits": 0,
"id": "block-v1:edX+DemoX+Demo_Course+type@html+block@html_49b4494da2f7",
"location": Array [
"location": [
"Example Week 3: Be Social",
"Lesson 3 - Be Social",
"Discussion Forums",
@@ -213,10 +213,10 @@ Object {
"type": "text",
"url": "/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@html+block@html_49b4494da2f7",
},
Object {
{
"contentHits": 0,
"id": "block-v1:edX+DemoX+Demo_Course+type@html+block@f4a39219742149f781a1dda6f43a623c",
"location": Array [
"location": [
"About Exams and Certificates",
"edX Exams",
"Overall Grade Performance",
@@ -226,10 +226,10 @@ Object {
"type": "text",
"url": "/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@html+block@f4a39219742149f781a1dda6f43a623c",
},
Object {
{
"contentHits": 0,
"id": "block-v1:edX+DemoX+Demo_Course+type@html+block@87fa6792d79f4862be098e5169e93339",
"location": Array [
"location": [
"Example Week 3: Be Social",
"Lesson 3 - Be Social",
"Homework - Find Your Study Buddy",
@@ -239,10 +239,10 @@ Object {
"type": "text",
"url": "/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@html+block@87fa6792d79f4862be098e5169e93339",
},
Object {
{
"contentHits": 0,
"id": "block-v1:edX+DemoX+Demo_Course+type@html+block@6018785795994726950614ce7d0f38c5",
"location": Array [
"location": [
"Example Week 3: Be Social",
"Homework - Find Your Study Buddy",
"Homework - Find Your Study Buddy",
@@ -252,10 +252,10 @@ Object {
"type": "text",
"url": "/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@html+block@6018785795994726950614ce7d0f38c5",
},
Object {
{
"contentHits": 0,
"id": "block-v1:edX+DemoX+Demo_Course+type@html+block@f9f3a25e7bab46e583fd1fbbd7a2f6a0",
"location": Array [
"location": [
"Example Week 3: Be Social",
"Lesson 3 - Be Social",
"Be Social",
@@ -265,10 +265,10 @@ Object {
"type": "text",
"url": "/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@html+block@f9f3a25e7bab46e583fd1fbbd7a2f6a0",
},
Object {
{
"contentHits": 0,
"id": "block-v1:edX+DemoX+Demo_Course+type@html+block@8293139743f34377817d537b69911530",
"location": Array [
"location": [
"About Exams and Certificates",
"edX Exams",
"EdX Exams",
@@ -278,10 +278,10 @@ Object {
"type": "text",
"url": "/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@html+block@8293139743f34377817d537b69911530",
},
Object {
{
"contentHits": 0,
"id": "block-v1:edX+DemoX+Demo_Course+type@html+block@9d5104b502f24ee89c3d2f4ce9d347cf",
"location": Array [
"location": [
"Example Week 1: Getting Started",
"Lesson 1 - Getting Started",
"When Are Your Exams? ",
@@ -291,7 +291,7 @@ Object {
"type": "text",
"url": "/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@html+block@9d5104b502f24ee89c3d2f4ce9d347cf",
},
Object {
{
"contentHits": 0,
"id": "random-element-id",
"location": null,

View File

@@ -6,7 +6,7 @@
right: 0;
bottom: 0;
border-top: 1px solid $light-300;
z-index: 200;
z-index: $zindex-modal; // Bootstrap's z-index layer for Modals.
&__close {
position: absolute !important; // For some reason it gets overridden
@@ -51,6 +51,8 @@
&__empty {
color: $gray-500;
padding: 6rem 0;
text-align: center;
}
&__item {

View File

@@ -70,12 +70,13 @@ export function useLockScroll() {
}, []);
}
const initSearchParams = { q: '', f: '' };
export function useCoursewareSearchParams() {
const [searchParams, setSearchParams] = useSearchParams();
const clearSearchParams = () => setSearchParams({ q: '', f: '' });
const [searchParams, setSearchParams] = useSearchParams(initSearchParams);
const clearSearchParams = () => setSearchParams(initSearchParams);
const query = searchParams.get('q');
const filter = searchParams.get('f');
const filter = searchParams.get('f')?.toLowerCase();
const setQuery = (q) => setSearchParams((params) => ({ q, f: params.get('f') }));
const setFilter = (f) => setSearchParams((params) => ({ q: params.get('q'), f }));

View File

@@ -1,9 +1,13 @@
import { renderHook, act } from '@testing-library/react-hooks';
import { useParams } from 'react-router-dom';
import { useParams, useSearchParams } from 'react-router-dom';
import { useSelector } from 'react-redux';
import { fetchCoursewareSearchSettings } from '../data/thunks';
import {
useCoursewareSearchFeatureFlag, useCoursewareSearchState, useElementBoundingBox, useLockScroll,
useCoursewareSearchFeatureFlag,
useCoursewareSearchParams,
useCoursewareSearchState,
useElementBoundingBox,
useLockScroll,
} from './hooks';
jest.mock('react-redux');
@@ -184,4 +188,45 @@ describe('CoursewareSearch Hooks', () => {
expect(removeBodyClassSpy).toHaveBeenCalledWith('_search-no-scroll');
});
});
describe('useSearchParams', () => {
const initSearch = { q: '', f: '' };
const q = { value: '' };
const f = { value: '' };
const mockedQuery = { q, f };
const searchParams = { get: (prop) => mockedQuery[prop].value };
const setSearchParams = jest.fn();
beforeEach(() => {
useSearchParams.mockImplementation(() => [searchParams, setSearchParams]);
});
it('should init the search params properly', () => {
const {
query, filter, setQuery, setFilter, clearSearchParams,
} = useCoursewareSearchParams();
expect(useSearchParams).toBeCalledWith(initSearch);
expect(query).toBe('');
expect(filter).toBe('');
setQuery('setQuery');
expect(setSearchParams).toBeCalledWith(expect.any(Function));
setFilter('setFilter');
expect(setSearchParams).toBeCalledWith(expect.any(Function));
clearSearchParams();
expect(setSearchParams).toBeCalledWith(initSearch);
});
it('should return the query and lowercase filter if any', () => {
q.value = '42';
f.value = 'LOWERCASE';
const { query, filter } = useCoursewareSearchParams();
expect(query).toBe('42');
expect(filter).toBe('lowercase');
});
});
});

View File

@@ -2,79 +2,84 @@ import { defineMessages } from '@edx/frontend-platform/i18n';
const messages = defineMessages({
searchOpenAction: {
id: 'learn.coursewareSerch.openAction',
id: 'learn.coursewareSearch.openAction',
defaultMessage: 'Search within this course',
description: 'Aria-label for a button that will pop up Courseware Search.',
},
contentSearchButton: {
id: 'learn.coursewareSearch.contentSearchButton',
defaultMessage: 'Content search',
description: 'Text for a button that will pop up Courseware Search.',
},
searchSubmitLabel: {
id: 'learn.coursewareSerch.submitLabel',
id: 'learn.coursewareSearch.submitLabel',
defaultMessage: 'Search',
description: 'Button label that will submit Courseware Search.',
},
searchClearAction: {
id: 'learn.coursewareSerch.clearAction',
id: 'learn.coursewareSearch.clearAction',
defaultMessage: 'Clear search',
description: 'Button label that will the current Courseware Search input.',
},
searchCloseAction: {
id: 'learn.coursewareSerch.closeAction',
id: 'learn.coursewareSearch.closeAction',
defaultMessage: 'Close the search form',
description: 'Aria-label for a button that will close Courseware Search.',
},
searchModuleTitle: {
id: 'learn.coursewareSerch.searchModuleTitle',
id: 'learn.coursewareSearch.searchModuleTitle',
defaultMessage: 'Search this course',
description: 'Title for the Courseware Search module.',
},
searchBarPlaceholderText: {
id: 'learn.coursewareSerch.searchBarPlaceholderText',
id: 'learn.coursewareSearch.searchBarPlaceholderText',
defaultMessage: 'Search',
description: 'Placeholder text for the Courseware Search input control',
},
loading: {
id: 'learn.coursewareSerch.loading',
id: 'learn.coursewareSearch.loading',
defaultMessage: 'Searching...',
description: 'Screen reader text to use on the spinner while the search is performing.',
},
searchResultsNone: {
id: 'learn.coursewareSerch.searchResultsNone',
id: 'learn.coursewareSearch.searchResultsNone',
defaultMessage: 'No results found.',
description: 'Text to show when the Courseware Search found no results matching the criteria.',
},
searchResultsLabel: {
id: 'learn.coursewareSerch.searchResultsLabel',
id: 'learn.coursewareSearch.searchResultsLabel',
defaultMessage: 'Results for "{keyword}":',
description: 'Text to show above the search results response list.',
},
searchResultsError: {
id: 'learn.coursewareSerch.searchResultsError',
id: 'learn.coursewareSearch.searchResultsError',
defaultMessage: 'There was an error on the search process. Please try again in a few minutes. If the problem persists, please contact the support team.',
description: 'Error message to show to the users when there\'s an error with the endpoint or the returned payload format.',
},
// These are translations for labeling the filters
'filter:all': {
id: 'learn.coursewareSerch.filter:all',
id: 'learn.coursewareSearch.filter:all',
defaultMessage: 'All content',
description: 'Label for the search results filter that shows all content (no filter).',
},
'filter:text': {
id: 'learn.coursewareSerch.filter:text',
id: 'learn.coursewareSearch.filter:text',
defaultMessage: 'Text',
description: 'Label for the search results filter that shows results with text content.',
},
'filter:video': {
id: 'learn.coursewareSerch.filter:video',
id: 'learn.coursewareSearch.filter:video',
defaultMessage: 'Video',
description: 'Label for the search results filter that shows results with video content.',
},
'filter:sequence': {
id: 'learn.coursewareSerch.filter:sequence',
id: 'learn.coursewareSearch.filter:sequence',
defaultMessage: 'Section',
description: 'Label for the search results filter that shows results with section content.',
},
'filter:other': {
id: 'learn.coursewareSerch.filter:other',
id: 'learn.coursewareSearch.filter:other',
defaultMessage: 'Other',
description: 'Label for the search results filter that shows results with other content.',
},

View File

@@ -6,6 +6,7 @@ Factory.define('courseHomeMetadata')
.option('host', 'http://localhost:18000')
.attrs({
title: 'Demonstration Course',
is_new_discussion_sidebar_view_enabled: false,
is_self_paced: false,
is_enrolled: false,
is_staff: false,

View File

@@ -1,8 +1,8 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Data layer integration tests Test fetchDatesTab Should fetch, normalize, and save metadata 1`] = `
Object {
"courseHome": Object {
{
"courseHome": {
"courseId": "course-v1:edX+DemoX+Demo_Course",
"courseStatus": "loaded",
"proctoringPanelStatus": "loading",
@@ -12,9 +12,13 @@ Object {
"toastBodyText": null,
"toastHeader": "",
},
"courseware": Object {
"courseware": {
"courseId": null,
"courseOutline": {},
"courseOutlineShouldUpdate": false,
"courseOutlineStatus": "loading",
"courseStatus": "loading",
"coursewareOutlineSidebarSettings": {},
"sequenceId": null,
"sequenceMightBeUnit": false,
"sequenceStatus": "loading",
@@ -22,12 +26,12 @@ Object {
"learningAssistant": ObjectContaining {
"conversationId": Any<String>,
},
"models": Object {
"courseHomeMeta": Object {
"course-v1:edX+DemoX+Demo_Course": Object {
"models": {
"courseHomeMeta": {
"course-v1:edX+DemoX+Demo_Course": {
"canViewCertificate": true,
"celebrations": null,
"courseAccess": Object {
"courseAccess": {
"additionalContextUserMessage": null,
"developerMessage": null,
"errorCode": null,
@@ -38,39 +42,40 @@ Object {
"id": "course-v1:edX+DemoX+Demo_Course",
"isEnrolled": false,
"isMasquerading": false,
"isNewDiscussionSidebarViewEnabled": false,
"isSelfPaced": false,
"isStaff": false,
"number": "DemoX",
"org": "edX",
"originalUserIsStaff": false,
"start": "2013-02-05T05:00:00Z",
"tabs": Array [
Object {
"tabs": [
{
"slug": "outline",
"title": "Course",
"url": "http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course/course/",
},
Object {
{
"slug": "discussion",
"title": "Discussion",
"url": "http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course/discussion/forum/",
},
Object {
{
"slug": "wiki",
"title": "Wiki",
"url": "http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course/course_wiki",
},
Object {
{
"slug": "progress",
"title": "Progress",
"url": "http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course/progress",
},
Object {
{
"slug": "instructor",
"title": "Instructor",
"url": "http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course/instructor",
},
Object {
{
"slug": "dates",
"title": "Dates",
"url": "http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course/dates",
@@ -79,7 +84,7 @@ Object {
"title": "Demonstration Course",
"userTimezone": "UTC",
"username": "MockUser",
"verifiedMode": Object {
"verifiedMode": {
"accessExpirationDate": null,
"currency": "USD",
"currencySymbol": "$",
@@ -89,10 +94,10 @@ Object {
},
},
},
"dates": Object {
"course-v1:edX+DemoX+Demo_Course": Object {
"courseDateBlocks": Array [
Object {
"dates": {
"course-v1:edX+DemoX+Demo_Course": {
"courseDateBlocks": [
{
"date": "2020-05-01T17:59:41Z",
"dateType": "course-start-date",
"description": "",
@@ -101,7 +106,7 @@ Object {
"link": "",
"title": "Course Starts",
},
Object {
{
"assignmentType": "Homework",
"complete": true,
"date": "2020-05-04T02:59:40.942669Z",
@@ -111,7 +116,7 @@ Object {
"learnerHasAccess": true,
"title": "Multi Badges Completed",
},
Object {
{
"assignmentType": "Homework",
"date": "2020-05-05T02:59:40.942669Z",
"dateType": "assignment-due-date",
@@ -120,7 +125,7 @@ Object {
"learnerHasAccess": true,
"title": "Multi Badges Past Due",
},
Object {
{
"assignmentType": "Homework",
"date": "2020-05-27T02:59:40.942669Z",
"dateType": "assignment-due-date",
@@ -130,7 +135,7 @@ Object {
"link": "https://example.com/",
"title": "Both Past Due 1",
},
Object {
{
"assignmentType": "Homework",
"date": "2020-05-27T02:59:40.942669Z",
"dateType": "assignment-due-date",
@@ -140,7 +145,7 @@ Object {
"link": "https://example.com/",
"title": "Both Past Due 2",
},
Object {
{
"assignmentType": "Homework",
"complete": true,
"date": "2020-05-28T08:59:40.942669Z",
@@ -151,7 +156,7 @@ Object {
"link": "https://example.com/",
"title": "One Completed/Due 1",
},
Object {
{
"assignmentType": "Homework",
"date": "2020-05-28T08:59:40.942669Z",
"dateType": "assignment-due-date",
@@ -161,7 +166,7 @@ Object {
"link": "https://example.com/",
"title": "One Completed/Due 2",
},
Object {
{
"assignmentType": "Homework",
"complete": true,
"date": "2020-05-29T08:59:40.942669Z",
@@ -172,7 +177,7 @@ Object {
"link": "https://example.com/",
"title": "Both Completed 1",
},
Object {
{
"assignmentType": "Homework",
"complete": true,
"date": "2020-05-29T08:59:40.942669Z",
@@ -183,7 +188,7 @@ Object {
"link": "https://example.com/",
"title": "Both Completed 2",
},
Object {
{
"date": "2020-06-16T17:59:40.942669Z",
"dateType": "verified-upgrade-deadline",
"description": "Don't miss the opportunity to highlight your new knowledge and skills by earning a verified certificate.",
@@ -192,7 +197,7 @@ Object {
"link": "https://example.com/",
"title": "Upgrade to Verified Certificate",
},
Object {
{
"assignmentType": "Homework",
"date": "2030-08-17T05:59:40.942669Z",
"dateType": "assignment-due-date",
@@ -202,7 +207,7 @@ Object {
"link": "https://example.com/",
"title": "One Verified 1",
},
Object {
{
"assignmentType": "Homework",
"date": "2030-08-17T05:59:40.942669Z",
"dateType": "assignment-due-date",
@@ -212,7 +217,7 @@ Object {
"link": "https://example.com/",
"title": "One Verified 2",
},
Object {
{
"assignmentType": "Homework",
"date": "2030-08-17T05:59:40.942669Z",
"dateType": "assignment-due-date",
@@ -222,7 +227,7 @@ Object {
"link": "https://example.com/",
"title": "ORA Verified 2",
},
Object {
{
"assignmentType": "Homework",
"date": "2030-08-18T05:59:40.942669Z",
"dateType": "assignment-due-date",
@@ -232,7 +237,7 @@ Object {
"link": "https://example.com/",
"title": "Both Verified 1",
},
Object {
{
"assignmentType": "Homework",
"date": "2030-08-18T05:59:40.942669Z",
"dateType": "assignment-due-date",
@@ -242,7 +247,7 @@ Object {
"link": "https://example.com/",
"title": "Both Verified 2",
},
Object {
{
"assignmentType": "Homework",
"date": "2030-08-19T05:59:40.942669Z",
"dateType": "assignment-due-date",
@@ -250,7 +255,7 @@ Object {
"learnerHasAccess": true,
"title": "One Unreleased 1",
},
Object {
{
"assignmentType": "Homework",
"date": "2030-08-19T05:59:40.942669Z",
"dateType": "assignment-due-date",
@@ -260,7 +265,7 @@ Object {
"link": "https://example.com/",
"title": "One Unreleased 2",
},
Object {
{
"assignmentType": "Homework",
"date": "2030-08-20T05:59:40.942669Z",
"dateType": "assignment-due-date",
@@ -269,7 +274,7 @@ Object {
"learnerHasAccess": true,
"title": "Both Unreleased 1",
},
Object {
{
"assignmentType": "Homework",
"date": "2030-08-20T05:59:40.942669Z",
"dateType": "assignment-due-date",
@@ -278,7 +283,7 @@ Object {
"learnerHasAccess": true,
"title": "Both Unreleased 2",
},
Object {
{
"date": "2030-08-23T00:00:00Z",
"dateType": "course-end-date",
"description": "",
@@ -287,7 +292,7 @@ Object {
"link": "",
"title": "Course Ends",
},
Object {
{
"date": "2030-09-01T00:00:00Z",
"dateType": "verification-deadline-date",
"description": "You must successfully complete verification before this date to qualify for a Verified Certificate.",
@@ -297,7 +302,7 @@ Object {
"title": "Verification Deadline",
},
],
"datesBannerInfo": Object {
"datesBannerInfo": {
"contentTypeGatingEnabled": false,
"missedDeadlines": false,
"missedGatedContent": false,
@@ -309,10 +314,75 @@ Object {
},
},
},
"recommendations": Object {
"plugins": {},
"recommendations": {
"recommendationsStatus": "loading",
},
"tours": Object {
"specialExams": {
"activeAttempt": null,
"allowProctoringOptOut": false,
"apiErrorMsg": "",
"exam": {
"attempt": {
"attempt_code": "",
"attempt_id": null,
"attempt_status": "",
"course_id": "",
"desktop_application_js_url": "",
"exam_display_name": "",
"exam_started_poll_url": "",
"exam_type": "",
"exam_url_path": "",
"external_id": "",
"in_timed_exam": true,
"ping_interval": null,
"taking_as_proctored": true,
"time_remaining_seconds": null,
"use_legacy_attempt_api": true,
},
"backend": "",
"content_id": "",
"course_id": "",
"due_date": null,
"exam_name": "",
"external_id": "",
"hide_after_due": false,
"id": null,
"is_active": true,
"is_practice_exam": false,
"is_proctored": false,
"prerequisite_status": {
"are_prerequisites_satisifed": true,
"declined_prerequisites": [],
"failed_prerequisites": [],
"pending_prerequisites": [],
"satisfied_prerequisites": [],
},
"time_limit_mins": null,
"type": "",
},
"examAccessToken": {
"exam_access_token": "",
"exam_access_token_expiration": "",
},
"isLoading": true,
"proctoringSettings": {
"exam_proctoring_backend": {
"download_url": "",
"instructions": [],
"name": "",
"rules": {},
},
"integration_specific_email": "",
"learner_notification_from_email": "",
"provider_name": "",
"provider_tech_support_email": "",
"provider_tech_support_phone": "",
"provider_tech_support_url": "",
},
"timeIsOver": false,
},
"tours": {
"showCoursewareTour": false,
"showExistingUserCourseHomeTour": false,
"showNewUserCourseHomeModal": false,
@@ -323,8 +393,8 @@ Object {
`;
exports[`Data layer integration tests Test fetchOutlineTab Should fetch, normalize, and save metadata 1`] = `
Object {
"courseHome": Object {
{
"courseHome": {
"courseId": "course-v1:edX+DemoX+Demo_Course",
"courseStatus": "loaded",
"proctoringPanelStatus": "loading",
@@ -334,9 +404,13 @@ Object {
"toastBodyText": null,
"toastHeader": "",
},
"courseware": Object {
"courseware": {
"courseId": null,
"courseOutline": {},
"courseOutlineShouldUpdate": false,
"courseOutlineStatus": "loading",
"courseStatus": "loading",
"coursewareOutlineSidebarSettings": {},
"sequenceId": null,
"sequenceMightBeUnit": false,
"sequenceStatus": "loading",
@@ -344,12 +418,12 @@ Object {
"learningAssistant": ObjectContaining {
"conversationId": Any<String>,
},
"models": Object {
"courseHomeMeta": Object {
"course-v1:edX+DemoX+Demo_Course": Object {
"models": {
"courseHomeMeta": {
"course-v1:edX+DemoX+Demo_Course": {
"canViewCertificate": true,
"celebrations": null,
"courseAccess": Object {
"courseAccess": {
"additionalContextUserMessage": null,
"developerMessage": null,
"errorCode": null,
@@ -360,39 +434,40 @@ Object {
"id": "course-v1:edX+DemoX+Demo_Course",
"isEnrolled": false,
"isMasquerading": false,
"isNewDiscussionSidebarViewEnabled": false,
"isSelfPaced": false,
"isStaff": false,
"number": "DemoX",
"org": "edX",
"originalUserIsStaff": false,
"start": "2013-02-05T05:00:00Z",
"tabs": Array [
Object {
"tabs": [
{
"slug": "outline",
"title": "Course",
"url": "http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course/course/",
},
Object {
{
"slug": "discussion",
"title": "Discussion",
"url": "http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course/discussion/forum/",
},
Object {
{
"slug": "wiki",
"title": "Wiki",
"url": "http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course/course_wiki",
},
Object {
{
"slug": "progress",
"title": "Progress",
"url": "http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course/progress",
},
Object {
{
"slug": "instructor",
"title": "Instructor",
"url": "http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course/instructor",
},
Object {
{
"slug": "dates",
"title": "Dates",
"url": "http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course/dates",
@@ -401,7 +476,7 @@ Object {
"title": "Demonstration Course",
"userTimezone": "UTC",
"username": "MockUser",
"verifiedMode": Object {
"verifiedMode": {
"accessExpirationDate": null,
"currency": "USD",
"currencySymbol": "$",
@@ -411,77 +486,80 @@ Object {
},
},
},
"outline": Object {
"course-v1:edX+DemoX+Demo_Course": Object {
"outline": {
"course-v1:edX+DemoX+Demo_Course": {
"accessExpiration": null,
"canShowUpgradeSock": false,
"certData": Object {
"certData": {
"certStatus": null,
"certWebViewUrl": null,
"certificateAvailableDate": null,
},
"courseBlocks": Object {
"courses": Object {
"block-v1:edX+DemoX+Demo_Course+type@course+block@bcdabcdabcdabcdabcdabcdabcdabcd3": Object {
"courseBlocks": {
"courses": {
"block-v1:edX+DemoX+Demo_Course+type@course+block@bcdabcdabcdabcdabcdabcdabcdabcd3": {
"hasScheduledContent": false,
"id": "course-v1:edX+DemoX+Demo_Course",
"sectionIds": Array [
"sectionIds": [
"block-v1:edX+DemoX+Demo_Course+type@chapter+block@bcdabcdabcdabcdabcdabcdabcdabcd2",
],
"title": "bcdabcdabcdabcdabcdabcdabcdabcd3",
},
},
"sections": Object {
"block-v1:edX+DemoX+Demo_Course+type@chapter+block@bcdabcdabcdabcdabcdabcdabcdabcd2": Object {
"sections": {
"block-v1:edX+DemoX+Demo_Course+type@chapter+block@bcdabcdabcdabcdabcdabcdabcdabcd2": {
"complete": false,
"courseId": "course-v1:edX+DemoX+Demo_Course",
"hideFromTOC": undefined,
"id": "block-v1:edX+DemoX+Demo_Course+type@chapter+block@bcdabcdabcdabcdabcdabcdabcdabcd2",
"resumeBlock": false,
"sequenceIds": Array [
"sequenceIds": [
"block-v1:edX+DemoX+Demo_Course+type@sequential+block@bcdabcdabcdabcdabcdabcdabcdabcd1",
],
"title": "Title of Section",
},
},
"sequences": Object {
"block-v1:edX+DemoX+Demo_Course+type@sequential+block@bcdabcdabcdabcdabcdabcdabcdabcd1": Object {
"sequences": {
"block-v1:edX+DemoX+Demo_Course+type@sequential+block@bcdabcdabcdabcdabcdabcdabcdabcd1": {
"complete": false,
"description": null,
"due": null,
"effortActivities": 2,
"effortTime": 15,
"hideFromTOC": undefined,
"icon": null,
"id": "block-v1:edX+DemoX+Demo_Course+type@sequential+block@bcdabcdabcdabcdabcdabcdabcdabcd1",
"navigationDisabled": undefined,
"sectionId": "block-v1:edX+DemoX+Demo_Course+type@chapter+block@bcdabcdabcdabcdabcdabcdabcdabcd2",
"showLink": true,
"title": "Title of Sequence",
},
},
},
"courseGoals": Object {
"courseGoals": {
"daysPerWeek": null,
"goalOptions": Array [],
"goalOptions": [],
"selectedGoal": null,
"subscribedToReminders": null,
"weeklyLearningGoalEnabled": false,
},
"courseTools": Array [
Object {
"courseTools": [
{
"analyticsId": "edx.bookmarks",
"title": "Bookmarks",
"url": "https://example.com/bookmarks",
},
],
"datesBannerInfo": Object {
"datesBannerInfo": {
"contentTypeGatingEnabled": false,
"missedDeadlines": false,
"missedGatedContent": false,
},
"datesWidget": Object {
"courseDateBlocks": Array [],
"datesWidget": {
"courseDateBlocks": [],
},
"enableProctoredExams": undefined,
"enrollAlert": Object {
"enrollAlert": {
"canEnroll": true,
"extraText": "Contact the administrator.",
},
@@ -491,13 +569,13 @@ Object {
"hasScheduledContent": null,
"id": "course-v1:edX+DemoX+Demo_Course",
"offer": null,
"resumeCourse": Object {
"resumeCourse": {
"hasVisitedCourse": false,
"url": "http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+Test+Block@12345abcde",
},
"timeOffsetMillis": 0,
"userHasPassingGrade": undefined,
"verifiedMode": Object {
"verifiedMode": {
"accessExpirationDate": "2050-01-01T12:00:00",
"currency": "USD",
"currencySymbol": "$",
@@ -509,10 +587,75 @@ Object {
},
},
},
"recommendations": Object {
"plugins": {},
"recommendations": {
"recommendationsStatus": "loading",
},
"tours": Object {
"specialExams": {
"activeAttempt": null,
"allowProctoringOptOut": false,
"apiErrorMsg": "",
"exam": {
"attempt": {
"attempt_code": "",
"attempt_id": null,
"attempt_status": "",
"course_id": "",
"desktop_application_js_url": "",
"exam_display_name": "",
"exam_started_poll_url": "",
"exam_type": "",
"exam_url_path": "",
"external_id": "",
"in_timed_exam": true,
"ping_interval": null,
"taking_as_proctored": true,
"time_remaining_seconds": null,
"use_legacy_attempt_api": true,
},
"backend": "",
"content_id": "",
"course_id": "",
"due_date": null,
"exam_name": "",
"external_id": "",
"hide_after_due": false,
"id": null,
"is_active": true,
"is_practice_exam": false,
"is_proctored": false,
"prerequisite_status": {
"are_prerequisites_satisifed": true,
"declined_prerequisites": [],
"failed_prerequisites": [],
"pending_prerequisites": [],
"satisfied_prerequisites": [],
},
"time_limit_mins": null,
"type": "",
},
"examAccessToken": {
"exam_access_token": "",
"exam_access_token_expiration": "",
},
"isLoading": true,
"proctoringSettings": {
"exam_proctoring_backend": {
"download_url": "",
"instructions": [],
"name": "",
"rules": {},
},
"integration_specific_email": "",
"learner_notification_from_email": "",
"provider_name": "",
"provider_tech_support_email": "",
"provider_tech_support_phone": "",
"provider_tech_support_url": "",
},
"timeIsOver": false,
},
"tours": {
"showCoursewareTour": false,
"showExistingUserCourseHomeTour": false,
"showNewUserCourseHomeModal": false,
@@ -523,8 +666,8 @@ Object {
`;
exports[`Data layer integration tests Test fetchProgressTab Should fetch, normalize, and save metadata 1`] = `
Object {
"courseHome": Object {
{
"courseHome": {
"courseId": "course-v1:edX+DemoX+Demo_Course",
"courseStatus": "loaded",
"proctoringPanelStatus": "loading",
@@ -534,9 +677,13 @@ Object {
"toastBodyText": null,
"toastHeader": "",
},
"courseware": Object {
"courseware": {
"courseId": null,
"courseOutline": {},
"courseOutlineShouldUpdate": false,
"courseOutlineStatus": "loading",
"courseStatus": "loading",
"coursewareOutlineSidebarSettings": {},
"sequenceId": null,
"sequenceMightBeUnit": false,
"sequenceStatus": "loading",
@@ -544,12 +691,12 @@ Object {
"learningAssistant": ObjectContaining {
"conversationId": Any<String>,
},
"models": Object {
"courseHomeMeta": Object {
"course-v1:edX+DemoX+Demo_Course": Object {
"models": {
"courseHomeMeta": {
"course-v1:edX+DemoX+Demo_Course": {
"canViewCertificate": true,
"celebrations": null,
"courseAccess": Object {
"courseAccess": {
"additionalContextUserMessage": null,
"developerMessage": null,
"errorCode": null,
@@ -560,39 +707,40 @@ Object {
"id": "course-v1:edX+DemoX+Demo_Course",
"isEnrolled": false,
"isMasquerading": false,
"isNewDiscussionSidebarViewEnabled": false,
"isSelfPaced": false,
"isStaff": false,
"number": "DemoX",
"org": "edX",
"originalUserIsStaff": false,
"start": "2013-02-05T05:00:00Z",
"tabs": Array [
Object {
"tabs": [
{
"slug": "outline",
"title": "Course",
"url": "http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course/course/",
},
Object {
{
"slug": "discussion",
"title": "Discussion",
"url": "http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course/discussion/forum/",
},
Object {
{
"slug": "wiki",
"title": "Wiki",
"url": "http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course/course_wiki",
},
Object {
{
"slug": "progress",
"title": "Progress",
"url": "http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course/progress",
},
Object {
{
"slug": "instructor",
"title": "Instructor",
"url": "http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course/instructor",
},
Object {
{
"slug": "dates",
"title": "Dates",
"url": "http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course/dates",
@@ -601,7 +749,7 @@ Object {
"title": "Demonstration Course",
"userTimezone": "UTC",
"username": "MockUser",
"verifiedMode": Object {
"verifiedMode": {
"accessExpirationDate": null,
"currency": "USD",
"currencySymbol": "$",
@@ -611,16 +759,16 @@ Object {
},
},
},
"progress": Object {
"course-v1:edX+DemoX+Demo_Course": Object {
"progress": {
"course-v1:edX+DemoX+Demo_Course": {
"accessExpiration": null,
"certificateData": Object {},
"completionSummary": Object {
"certificateData": {},
"completionSummary": {
"completeCount": 1,
"incompleteCount": 1,
"lockedCount": 0,
},
"courseGrade": Object {
"courseGrade": {
"isPassing": true,
"letterGrade": "pass",
"percent": 1,
@@ -631,10 +779,10 @@ Object {
"enrollmentMode": "audit",
"gradesFeatureIsFullyLocked": false,
"gradesFeatureIsPartiallyLocked": false,
"gradingPolicy": Object {
"assignmentPolicies": Array [
Object {
"averageGrade": "1.00",
"gradingPolicy": {
"assignmentPolicies": [
{
"averageGrade": "1.0000",
"numDroppable": 1,
"shortLabel": "HW",
"type": "Homework",
@@ -642,17 +790,17 @@ Object {
"weightedGrade": 1,
},
],
"gradeRange": Object {
"gradeRange": {
"pass": 0.75,
},
},
"hasScheduledContent": false,
"id": "course-v1:edX+DemoX+Demo_Course",
"sectionScores": Array [
Object {
"sectionScores": [
{
"displayName": "First section",
"subsections": Array [
Object {
"subsections": [
{
"assignmentType": "Homework",
"blockKey": "block-v1:edX+DemoX+Demo_Course+type@sequential+block@12345",
"displayName": "First subsection",
@@ -661,16 +809,16 @@ Object {
"numPointsEarned": 0,
"numPointsPossible": 3,
"percentGraded": 0,
"problemScores": Array [
Object {
"problemScores": [
{
"earned": 0,
"possible": 1,
},
Object {
{
"earned": 0,
"possible": 1,
},
Object {
{
"earned": 0,
"possible": 1,
},
@@ -681,18 +829,18 @@ Object {
},
],
},
Object {
{
"displayName": "Second section",
"subsections": Array [
Object {
"subsections": [
{
"assignmentType": "Homework",
"displayName": "Second subsection",
"hasGradedAssignment": true,
"numPointsEarned": 1,
"numPointsPossible": 1,
"percentGraded": 1,
"problemScores": Array [
Object {
"problemScores": [
{
"earned": 1,
"possible": 1,
},
@@ -706,7 +854,7 @@ Object {
],
"studioUrl": "http://studio.edx.org/settings/grading/course-v1:edX+Test+run",
"userHasPassingGrade": false,
"verificationData": Object {
"verificationData": {
"link": null,
"status": "none",
"statusDate": null,
@@ -715,10 +863,75 @@ Object {
},
},
},
"recommendations": Object {
"plugins": {},
"recommendations": {
"recommendationsStatus": "loading",
},
"tours": Object {
"specialExams": {
"activeAttempt": null,
"allowProctoringOptOut": false,
"apiErrorMsg": "",
"exam": {
"attempt": {
"attempt_code": "",
"attempt_id": null,
"attempt_status": "",
"course_id": "",
"desktop_application_js_url": "",
"exam_display_name": "",
"exam_started_poll_url": "",
"exam_type": "",
"exam_url_path": "",
"external_id": "",
"in_timed_exam": true,
"ping_interval": null,
"taking_as_proctored": true,
"time_remaining_seconds": null,
"use_legacy_attempt_api": true,
},
"backend": "",
"content_id": "",
"course_id": "",
"due_date": null,
"exam_name": "",
"external_id": "",
"hide_after_due": false,
"id": null,
"is_active": true,
"is_practice_exam": false,
"is_proctored": false,
"prerequisite_status": {
"are_prerequisites_satisifed": true,
"declined_prerequisites": [],
"failed_prerequisites": [],
"pending_prerequisites": [],
"satisfied_prerequisites": [],
},
"time_limit_mins": null,
"type": "",
},
"examAccessToken": {
"exam_access_token": "",
"exam_access_token_expiration": "",
},
"isLoading": true,
"proctoringSettings": {
"exam_proctoring_backend": {
"download_url": "",
"instructions": [],
"name": "",
"rules": {},
},
"integration_specific_email": "",
"learner_notification_from_email": "",
"provider_name": "",
"provider_tech_support_email": "",
"provider_tech_support_phone": "",
"provider_tech_support_url": "",
},
"timeIsOver": false,
},
"tours": {
"showCoursewareTour": false,
"showExistingUserCourseHomeTour": false,
"showNewUserCourseHomeModal": false,

View File

@@ -18,7 +18,7 @@ const calculateAssignmentTypeGrades = (points, assignmentWeight, numDroppable) =
// Calculate the average grade for the assignment and round it. This rounding is not ideal and does not accurately
// reflect what a learner's grade would be, however, we must have parity with the current grading behavior that
// exists in edx-platform.
averageGrade = (points.reduce((a, b) => a + b, 0) / points.length).toFixed(2);
averageGrade = (points.reduce((a, b) => a + b, 0) / points.length).toFixed(4);
weightedGrade = averageGrade * assignmentWeight;
}
return { averageGrade, weightedGrade };
@@ -136,6 +136,7 @@ export function normalizeOutlineBlocks(courseId, blocks) {
title: block.display_name,
resumeBlock: block.resume_block,
sequenceIds: block.children || [],
hideFromTOC: block.hide_from_toc,
};
break;
@@ -152,6 +153,8 @@ export function normalizeOutlineBlocks(courseId, blocks) {
// link in the outline (even though we ignore the given url and use an internal <Link> to ourselves).
showLink: !!block.lms_web_url,
title: block.display_name,
hideFromTOC: block.hide_from_toc,
navigationDisabled: block.navigation_disabled,
};
break;

View File

@@ -55,6 +55,29 @@ describe('Data layer integration tests', () => {
expect(store.getState().courseHome.courseStatus).toEqual('failed');
});
it('should result in fetch failed if course metadata call errored', async () => {
const datesTabData = Factory.build('datesTabData');
const datesUrl = `${datesBaseUrl}/${courseId}`;
axiosMock.onGet(courseMetadataUrl).networkError();
axiosMock.onGet(datesUrl).reply(200, datesTabData);
await executeThunk(thunks.fetchDatesTab(courseId), store.dispatch);
expect(loggingService.logError).toHaveBeenCalled();
expect(store.getState().courseHome.courseStatus).toEqual('failed');
});
it('should result in fetch failed if course metadata call errored', async () => {
axiosMock.onGet(courseMetadataUrl).reply(200, courseHomeMetadata);
axiosMock.onGet(`${datesBaseUrl}/${courseId}`).networkError();
await executeThunk(thunks.fetchDatesTab(courseId), store.dispatch);
expect(loggingService.logError).toHaveBeenCalled();
expect(store.getState().courseHome.courseStatus).toEqual('failed');
});
it('Should fetch, normalize, and save metadata', async () => {
const datesTabData = Factory.build('datesTabData');
@@ -78,18 +101,14 @@ describe('Data layer integration tests', () => {
});
it.each([401, 403, 404])(
'should result in fetch denied for expected errors and failed for all others',
'should result in fetch denied if course access is denied, regardless of dates API status',
async (errorStatus) => {
axiosMock.onGet(courseMetadataUrl).reply(200, courseHomeAccessDeniedMetadata);
axiosMock.onGet(`${datesBaseUrl}/${courseId}`).reply(errorStatus, {});
await executeThunk(thunks.fetchDatesTab(courseId), store.dispatch);
let expectedState = 'failed';
if (errorStatus === 401 || errorStatus === 403) {
expectedState = 'denied';
}
expect(store.getState().courseHome.courseStatus).toEqual(expectedState);
expect(store.getState().courseHome.courseStatus).toEqual('denied');
},
);
});
@@ -129,18 +148,14 @@ describe('Data layer integration tests', () => {
});
it.each([401, 403, 404])(
'should result in fetch denied for expected errors and failed for all others',
'should result in fetch denied if course access is denied, regardless of outline API status',
async (errorStatus) => {
axiosMock.onGet(courseMetadataUrl).reply(200, courseHomeAccessDeniedMetadata);
axiosMock.onGet(outlineUrl).reply(errorStatus, {});
await executeThunk(thunks.fetchOutlineTab(courseId), store.dispatch);
let expectedState = 'failed';
if (errorStatus === 403) {
expectedState = 'denied';
}
expect(store.getState().courseHome.courseStatus).toEqual(expectedState);
expect(store.getState().courseHome.courseStatus).toEqual('denied');
},
);
});

View File

@@ -38,28 +38,41 @@ export function fetchTab(courseId, tab, getTabData, targetUserId) {
return async (dispatch) => {
dispatch(fetchTabRequest({ courseId }));
try {
const courseHomeCourseMetadata = await getCourseHomeCourseMetadata(courseId, 'outline');
dispatch(addModel({
modelType: 'courseHomeMeta',
model: {
id: courseId,
...courseHomeCourseMetadata,
},
}));
const tabDataResult = getTabData && await getTabData(courseId, targetUserId);
if (tabDataResult) {
const promisesToFulfill = [getCourseHomeCourseMetadata(courseId, 'outline')];
if (getTabData) {
promisesToFulfill.push(getTabData(courseId, targetUserId));
}
const [
courseHomeCourseMetadataResult,
tabDataResult,
] = await Promise.allSettled(promisesToFulfill);
if (courseHomeCourseMetadataResult.status === 'fulfilled') {
dispatch(addModel({
modelType: 'courseHomeMeta',
model: {
id: courseId,
...courseHomeCourseMetadataResult.value,
},
}));
}
if (tabDataResult?.status === 'fulfilled') {
dispatch(addModel({
modelType: tab,
model: {
id: courseId,
...tabDataResult,
...tabDataResult.value,
},
}));
}
// Disable the access-denied path for now - it caused a regression
if (!courseHomeCourseMetadata.courseAccess.hasAccess) {
if (courseHomeCourseMetadataResult.status === 'rejected') {
throw courseHomeCourseMetadataResult.reason;
} else if (!courseHomeCourseMetadataResult.value.courseAccess.hasAccess) {
// If the learner does not have access to the course, short cut to dispatch to a denied response regardless of
// the tabDataResult.
dispatch(fetchTabDenied({ courseId }));
} else if (tabDataResult || !getTabData) {
} else if (tabDataResult?.status === 'rejected') {
throw tabDataResult.reason;
} else {
dispatch(fetchTabSuccess({
courseId,
targetUserId,

View File

@@ -8,7 +8,7 @@ import {
injectIntl,
intlShape,
} from '@edx/frontend-platform/i18n';
import { Tooltip, OverlayTrigger } from '@edx/paragon';
import { Tooltip, OverlayTrigger } from '@openedx/paragon';
import { faInfoCircle } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';

View File

@@ -2,7 +2,7 @@ import React from 'react';
import classNames from 'classnames';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faLock } from '@fortawesome/free-solid-svg-icons';
import { Badge } from '@edx/paragon';
import { Badge } from '@openedx/paragon';
import messages from '../messages';
import { daycmp, isLearnerAssignment } from '../utils';
@@ -38,21 +38,21 @@ function getBadgeListAndColor(date, intl, item, items) {
message: messages.today,
shownForDay: isToday,
bg: 'bg-warning-300',
className: 'text-black',
className: 'text-dark',
},
{
message: messages.completed,
shownForDay: assignments.length && assignments.every(isComplete),
shownForItem: x => isLearnerAssignment(x) && isComplete(x),
bg: 'bg-light-500',
className: 'text-black',
className: 'text-dark',
},
{
message: messages.pastDue,
shownForDay: assignments.length && assignments.every(isPastDue),
shownForItem: x => isLearnerAssignment(x) && isPastDue(x),
bg: 'bg-dark-200',
className: 'text-white',
className: 'text-dark',
},
{
message: messages.dueNext,

View File

@@ -1,7 +1,7 @@
import PropTypes from 'prop-types';
import { getConfig } from '@edx/frontend-platform';
import { FormattedMessage, injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { Button, Hyperlink } from '@edx/paragon';
import { Button, Hyperlink } from '@openedx/paragon';
import messages from './messages';
import { ReactComponent as UnsubscribeIcon } from './unsubscribe.svg';

View File

@@ -9,8 +9,9 @@ const LmsHtmlFragment = ({
title,
...rest
}) => {
const direction = document.documentElement?.getAttribute('dir') || 'ltr';
const wholePage = `
<html>
<html dir="${direction}">
<head>
<base href="${getConfig().LMS_BASE_URL}" target="_parent">
<link rel="stylesheet" href="/static/${getConfig().LEGACY_THEME_NAME ? `${getConfig().LEGACY_THEME_NAME}/` : ''}css/bootstrap/lms-main.css">
@@ -29,7 +30,7 @@ const LmsHtmlFragment = ({
const iframe = useRef(null);
function resetIframeHeight() {
if (iframe?.current?.contentWindow?.document?.body) {
iframe.current.height = iframe.current.contentWindow.document.body.scrollHeight;
iframe.current.height = iframe.current.contentWindow.document.body.parentNode.scrollHeight;
}
}

View File

@@ -4,7 +4,8 @@ import { useSelector } from 'react-redux';
import { sendTrackEvent } from '@edx/frontend-platform/analytics';
import { getAuthenticatedUser } from '@edx/frontend-platform/auth';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { Button } from '@edx/paragon';
import { Button } from '@openedx/paragon';
import { PluginSlot } from '@openedx/frontend-plugin-framework';
import { AlertList } from '../../generic/user-messages';
import CourseDates from './widgets/CourseDates';
@@ -194,19 +195,24 @@ const OutlineTab = ({ intl }) => {
/>
)}
<CourseTools />
<UpgradeNotification
offer={offer}
verifiedMode={verifiedMode}
accessExpiration={accessExpiration}
contentTypeGatingEnabled={datesBannerInfo.contentTypeGatingEnabled}
marketingUrl={marketingUrl}
upsellPageName="course_home"
userTimezone={userTimezone}
shouldDisplayBorder
timeOffsetMillis={timeOffsetMillis}
courseId={courseId}
org={org}
/>
<PluginSlot
id="outline_tab_notifications_slot"
pluginProps={{ courseId }}
>
<UpgradeNotification
offer={offer}
verifiedMode={verifiedMode}
accessExpiration={accessExpiration}
contentTypeGatingEnabled={datesBannerInfo.contentTypeGatingEnabled}
marketingUrl={marketingUrl}
upsellPageName="course_home"
userTimezone={userTimezone}
shouldDisplayBorder
timeOffsetMillis={timeOffsetMillis}
courseId={courseId}
org={org}
/>
</PluginSlot>
<CourseDates />
<CourseHandouts />
</div>

View File

@@ -132,6 +132,16 @@ describe('Outline Tab', () => {
expect(expandedSectionNode).toHaveAttribute('aria-expanded', 'true');
});
it('includes outline_tab_notifications_slot', async () => {
const { courseBlocks } = await buildMinimalCourseBlocks(courseId, 'Title', { resumeBlock: true });
setTabData({
course_blocks: { blocks: courseBlocks.blocks },
});
await fetchAndRender();
expect(screen.getByTestId('outline_tab_notifications_slot')).toBeInTheDocument();
});
it('handles expand/collapse all button click', async () => {
await fetchAndRender();
// Button renders as "Expand All"
@@ -282,6 +292,22 @@ describe('Outline Tab', () => {
});
});
it('ignores comments and misformatted HTML', async () => {
setTabData({
welcome_message_html: '<p class="additional-spaces-in-tag" >'
+ '<!-- Even if the welcome_message_html length is above the limit because of comments, we hope it will not be shortened. -->'
+ '<!-- Even if the welcome_message_html length is above the limit because of comments, we hope it will not be shortened. -->'
+ 'Test welcome message that happens to be longer than one hundred words because of comments but displayed content is less.'
+ 'It should not be shortened.'
+ '<!-- Even if the welcome_message_html length is above the limit because of comments, we hope it will not be shortened. -->'
+ '<!-- Even if the welcome_message_html length is above the limit because of comments, we hope it will not be shortened. -->'
+ '</p>',
});
await fetchAndRender();
const showMoreButton = screen.queryByRole('button', { name: 'Show More' });
expect(showMoreButton).not.toBeInTheDocument();
});
it('does not display if no update available', async () => {
setTabData({ welcome_message_html: null });
await fetchAndRender();
@@ -1269,5 +1295,97 @@ describe('Outline Tab', () => {
await waitFor(() => expect(axiosMock.history.post).toHaveLength(1));
expect(axiosMock.history.post[0].url).toEqual(resendEmailUrl);
});
it('section should show hidden from toc message when hide_from_toc is true', async () => {
const { courseBlocks } = await buildMinimalCourseBlocks(courseId, 'Title', { resumeBlock: true });
const courseBlocksIds = Object.keys(courseBlocks.blocks);
const newCourseBlocks = courseBlocksIds.reduce((blocks, blockId) => ({
...blocks,
[blockId]: {
...courseBlocks.blocks[blockId],
hide_from_toc: true,
},
}), {});
setTabData({
course_blocks: { blocks: newCourseBlocks },
});
await fetchAndRender();
const iconHiddenFromTocSectionNode = screen.getByTestId('hide-from-toc-section-icon');
const textHiddenFromTocSectionNode = screen.getByTestId('hide-from-toc-section-text');
expect(iconHiddenFromTocSectionNode).toBeInTheDocument();
expect(textHiddenFromTocSectionNode).toBeInTheDocument();
expect(textHiddenFromTocSectionNode.textContent).toBe('Hidden in Course Outline, accessible via link');
});
it('section should not show hidden from toc message when hide_from_toc is false', async () => {
const { courseBlocks } = await buildMinimalCourseBlocks(courseId, 'Title', { resumeBlock: true });
const courseBlocksIds = Object.keys(courseBlocks.blocks);
const newCourseBlocks = courseBlocksIds.reduce((blocks, blockId) => ({
...blocks,
[blockId]: {
...courseBlocks.blocks[blockId],
hide_from_toc: false,
},
}), {});
setTabData({
course_blocks: { blocks: newCourseBlocks },
});
await fetchAndRender();
const iconHiddenFromTocSectionNode = screen.queryByTestId('hide-from-toc-section-icon');
const textHiddenFromTocSectionNode = screen.queryByTestId('hide-from-toc-section-text');
expect(iconHiddenFromTocSectionNode).not.toBeInTheDocument();
expect(textHiddenFromTocSectionNode).not.toBeInTheDocument();
});
it('sequence link should show hidden from toc message when hide_from_toc is true', async () => {
const { courseBlocks } = await buildMinimalCourseBlocks(courseId, 'Title', { resumeBlock: true });
const courseBlocksIds = Object.keys(courseBlocks.blocks);
const newCourseBlocks = courseBlocksIds.reduce((blocks, blockId) => ({
...blocks,
[blockId]: {
...courseBlocks.blocks[blockId],
hide_from_toc: true,
},
}), {});
setTabData({
course_blocks: { blocks: newCourseBlocks },
});
await fetchAndRender();
const iconHiddenFromTocSequenceLinkNode = screen.getByTestId('hide-from-toc-sequence-link-icon');
const textHiddenFromTocSequenceLink = screen.getByTestId('hide-from-toc-sequence-link-text');
expect(iconHiddenFromTocSequenceLinkNode).toBeInTheDocument();
expect(textHiddenFromTocSequenceLink).toBeInTheDocument();
expect(textHiddenFromTocSequenceLink.textContent).toBe('Subsections are not navigable between each other, they can only be accessed through their link.');
});
it('sequence link not show hidden from toc message when hide_from_toc is false', async () => {
const { courseBlocks } = await buildMinimalCourseBlocks(courseId, 'Title', { resumeBlock: true });
const courseBlocksIds = Object.keys(courseBlocks.blocks);
const newCourseBlocks = courseBlocksIds.reduce((blocks, blockId) => ({
...blocks,
[blockId]: {
...courseBlocks.blocks[blockId],
hide_from_toc: false,
},
}), {});
setTabData({
course_blocks: { blocks: newCourseBlocks },
});
await fetchAndRender();
const iconHiddenFromTocSequenceLink = screen.queryByTestId('hide-from-toc-sequence-link-icon');
const textHiddenFromTocSequenceLink = screen.queryByTestId('hide-from-toc-sequence-link-text');
expect(iconHiddenFromTocSequenceLink).not.toBeInTheDocument();
expect(textHiddenFromTocSequenceLink).not.toBeInTheDocument();
});
});
});

View File

@@ -1,11 +1,12 @@
import React, { useEffect, useState } from 'react';
import PropTypes from 'prop-types';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { Collapsible, IconButton } from '@edx/paragon';
import { Collapsible, IconButton, Icon } from '@openedx/paragon';
import { faCheckCircle as fasCheckCircle, faMinus, faPlus } from '@fortawesome/free-solid-svg-icons';
import { faCheckCircle as farCheckCircle } from '@fortawesome/free-regular-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { DisabledVisible } from '@openedx/paragon/icons';
import SequenceLink from './SequenceLink';
import { useModel } from '../../generic/model-store';
@@ -23,6 +24,7 @@ const Section = ({
complete,
sequenceIds,
title,
hideFromTOC,
} = section;
const {
courseBlocks: {
@@ -42,7 +44,7 @@ const Section = ({
}, []);
const sectionTitle = (
<div className="row w-100 m-0">
<div className="d-flex row w-100 m-0">
<div className="col-auto p-0">
{complete ? (
<FontAwesomeIcon
@@ -62,12 +64,24 @@ const Section = ({
/>
)}
</div>
<div className="col-10 ml-3 p-0 font-weight-bold text-dark-500">
<span className="align-middle">{title}</span>
<div className="col-7 ml-3 p-0 font-weight-bold text-dark-500">
<span className="align-middle col-6">{title}</span>
<span className="sr-only">
, {intl.formatMessage(complete ? messages.completedSection : messages.incompleteSection)}
</span>
</div>
{hideFromTOC && (
<div className="row">
{hideFromTOC && (
<span className="small d-flex align-content-end">
<Icon className="mr-2" src={DisabledVisible} data-testid="hide-from-toc-section-icon" />
<span data-testid="hide-from-toc-section-text">
{intl.formatMessage(messages.hiddenSection)}
</span>
</span>
)}
</div>
)}
</div>
);

View File

@@ -1,4 +1,3 @@
import React from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
import { Link } from 'react-router-dom';
@@ -12,6 +11,8 @@ import { faCheckCircle as fasCheckCircle } from '@fortawesome/free-solid-svg-ico
import { faCheckCircle as farCheckCircle } from '@fortawesome/free-regular-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { Icon } from '@openedx/paragon';
import { Block } from '@openedx/paragon/icons';
import EffortEstimate from '../../shared/effort-estimate';
import { useModel } from '../../generic/model-store';
import messages from './messages';
@@ -29,6 +30,7 @@ const SequenceLink = ({
due,
showLink,
title,
hideFromTOC,
} = sequence;
const {
userTimezone,
@@ -93,7 +95,7 @@ const SequenceLink = ({
icon={fasCheckCircle}
fixedWidth
className="float-left text-success mt-1"
aria-hidden="true"
aria-hidden={complete}
title={intl.formatMessage(messages.completedAssignment)}
/>
) : (
@@ -101,7 +103,7 @@ const SequenceLink = ({
icon={farCheckCircle}
fixedWidth
className="float-left text-gray-400 mt-1"
aria-hidden="true"
aria-hidden={complete}
title={intl.formatMessage(messages.incompleteAssignment)}
/>
)}
@@ -114,6 +116,16 @@ const SequenceLink = ({
<EffortEstimate className="ml-3 align-middle" block={sequence} />
</div>
</div>
{hideFromTOC && (
<div className="row w-100 my-2 mx-4 pl-3">
<span className="small d-flex">
<Icon className="mr-2" src={Block} data-testid="hide-from-toc-sequence-link-icon" />
<span data-testid="hide-from-toc-sequence-link-text">
{intl.formatMessage(messages.hiddenSequenceLink)}
</span>
</span>
</div>
)}
<div className="row w-100 m-0 ml-3 pl-3">
<small className="text-body pl-2">
{due ? dueDateMessage : noDueDateMessage}

View File

@@ -6,7 +6,7 @@ import {
injectIntl,
intlShape,
} from '@edx/frontend-platform/i18n';
import { Alert, Button } from '@edx/paragon';
import { Alert, Button } from '@openedx/paragon';
import { useDispatch } from 'react-redux';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';

View File

@@ -6,8 +6,8 @@ import {
FormattedRelativeTime,
FormattedTime,
} from '@edx/frontend-platform/i18n';
import { Alert } from '@edx/paragon';
import { Info } from '@edx/paragon/icons';
import { Alert } from '@openedx/paragon';
import { Info } from '@openedx/paragon/icons';
const DAY_SEC = 24 * 60 * 60; // in seconds
const DAY_MS = DAY_SEC * 1000; // in ms

View File

@@ -3,7 +3,7 @@ import PropTypes from 'prop-types';
import { getConfig } from '@edx/frontend-platform';
import { injectIntl, intlShape, FormattedMessage } from '@edx/frontend-platform/i18n';
import { getLoginRedirectUrl } from '@edx/frontend-platform/auth';
import { Alert, Button, Hyperlink } from '@edx/paragon';
import { Alert, Button, Hyperlink } from '@openedx/paragon';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faSpinner } from '@fortawesome/free-solid-svg-icons';

View File

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

View File

@@ -36,6 +36,16 @@ const messages = defineMessages({
defaultMessage: 'Completed section',
description: 'Text used to describe the green checkmark icon in front of a section title',
},
hiddenSection: {
id: 'learning.outline.hiddenSection',
defaultMessage: 'Hidden in Course Outline, accessible via link',
description: 'Label for hidden section in course outline',
},
hiddenSequenceLink: {
id: 'learning.outline.hiddenSequenceLink',
defaultMessage: 'Subsections are not navigable between each other, they can only be accessed through their link.',
description: 'Label for hidden sequence in course outline',
},
dates: {
id: 'learning.outline.dates',
defaultMessage: 'Important dates',

View File

@@ -1,5 +1,5 @@
@import "~@edx/brand/paragon/variables";
@import "~@edx/paragon/scss/core/core";
@import "~@openedx/paragon/scss/core/core";
@import "~@edx/brand/paragon/overrides";
.flag-button {

View File

@@ -3,7 +3,7 @@ import { useDispatch, useSelector } from 'react-redux';
import camelCase from 'lodash.camelcase';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { Button } from '@edx/paragon';
import { Button } from '@openedx/paragon';
import messages from '../messages';
import { getProctoringInfoData } from '../../data/api';

View File

@@ -1,5 +1,5 @@
import React from 'react';
import { Button, Card } from '@edx/paragon';
import { Button, Card } from '@openedx/paragon';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { useSelector } from 'react-redux';

View File

@@ -2,12 +2,12 @@ import React, { useEffect, useState } from 'react';
import { useLocation } from 'react-router-dom';
import PropTypes from 'prop-types';
import { Form, Card, Icon } from '@edx/paragon';
import { Form, Card, Icon } from '@openedx/paragon';
import { history } from '@edx/frontend-platform';
import { sendTrackEvent } from '@edx/frontend-platform/analytics';
import { getAuthenticatedUser } from '@edx/frontend-platform/auth';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { Email } from '@edx/paragon/icons';
import { Email } from '@openedx/paragon/icons';
import { useSelector } from 'react-redux';
import messages from '../messages';
import LearningGoalButton from './LearningGoalButton';

View File

@@ -1,8 +1,8 @@
import React, { useState } from 'react';
import React, { useState, useMemo } from 'react';
import PropTypes from 'prop-types';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { Alert, Button, TransitionReplace } from '@edx/paragon';
import { Alert, Button, TransitionReplace } from '@openedx/paragon';
import truncate from 'truncate-html';
import { useDispatch } from 'react-redux';
@@ -18,8 +18,22 @@ const WelcomeMessage = ({ courseId, intl }) => {
const [display, setDisplay] = useState(true);
const shortWelcomeMessageHtml = truncate(welcomeMessageHtml, 100, { byWords: true, keepWhitespaces: true });
const messageCanBeShortened = shortWelcomeMessageHtml.length < welcomeMessageHtml.length;
// welcomeMessageHtml can contain comments or malformatted HTML which can impact the length that determines
// messageCanBeShortened. We clean it by calling truncate with a length of welcomeMessageHtml.length which
// will not result in a truncation but a formatting into 'truncate-html' canonical format.
const cleanedWelcomeMessageHtml = useMemo(
() => truncate(welcomeMessageHtml, welcomeMessageHtml.length, { keepWhitespaces: true }),
[welcomeMessageHtml],
);
const shortWelcomeMessageHtml = useMemo(
() => truncate(cleanedWelcomeMessageHtml, 100, { byWords: true, keepWhitespaces: true }),
[cleanedWelcomeMessageHtml],
);
const messageCanBeShortened = useMemo(
() => (shortWelcomeMessageHtml.length < cleanedWelcomeMessageHtml.length),
[cleanedWelcomeMessageHtml, shortWelcomeMessageHtml],
);
const [showShortMessage, setShowShortMessage] = useState(messageCanBeShortened);
const dispatch = useDispatch();
@@ -63,7 +77,7 @@ const WelcomeMessage = ({ courseId, intl }) => {
className="inline-link"
data-testid="long-welcome-message-iframe"
key="full-html"
html={welcomeMessageHtml}
html={cleanedWelcomeMessageHtml}
title={intl.formatMessage(messages.welcomeMessage)}
/>
)}

View File

@@ -3,7 +3,7 @@ import { useSelector } from 'react-redux';
import { getAuthenticatedUser } from '@edx/frontend-platform/auth';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { Button } from '@edx/paragon';
import { Button } from '@openedx/paragon';
import { useModel } from '../../generic/model-store';

View File

@@ -1,6 +1,6 @@
import React from 'react';
import { useSelector } from 'react-redux';
import { breakpoints, useWindowSize } from '@edx/paragon';
import { breakpoints, useWindowSize } from '@openedx/paragon';
import CertificateStatus from './certificate-status/CertificateStatus';
import CourseCompletion from './course-completion/CourseCompletion';

View File

@@ -3,7 +3,7 @@ import { Factory } from 'rosie';
import { getConfig } from '@edx/frontend-platform';
import { sendTrackEvent } from '@edx/frontend-platform/analytics';
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
import { breakpoints } from '@edx/paragon';
import { breakpoints } from '@openedx/paragon';
import MockAdapter from 'axios-mock-adapter';
import {

View File

@@ -6,7 +6,7 @@ import {
FormattedDate, FormattedMessage, injectIntl, intlShape,
} from '@edx/frontend-platform/i18n';
import { Button, Card } from '@edx/paragon';
import { Button, Card } from '@openedx/paragon';
import { getConfig } from '@edx/frontend-platform';
import { useModel } from '../../../generic/model-store';
import { COURSE_EXIT_MODES, getCourseExitMode } from '../../../courseware/course/course-exit/utils';
@@ -19,6 +19,10 @@ const CertificateStatus = ({ intl }) => {
courseId,
} = useSelector(state => state.courseHome);
const {
entranceExamData,
} = useModel('coursewareMeta', courseId);
const {
isEnrolled,
org,
@@ -42,6 +46,8 @@ const CertificateStatus = ({ intl }) => {
certificateAvailableDate,
} = certificateData || {};
const entranceExamPassed = entranceExamData?.entranceExamPassed ?? null;
const mode = getCourseExitMode(
certificateData,
hasScheduledContent,
@@ -49,6 +55,7 @@ const CertificateStatus = ({ intl }) => {
userHasPassingGrade,
null, // CourseExitPageIsActive
canViewCertificate,
entranceExamPassed,
);
const eventProperties = {
@@ -154,7 +161,7 @@ const CertificateStatus = ({ intl }) => {
certAvailabilityDate = <FormattedDate value={certificateAvailableDate} day="numeric" month="long" year="numeric" />;
body = (
<FormattedMessage
id="courseCelebration.certificateBody.notAvailable.endDate"
id="progress.certificateStatus.notAvailable.endDate"
defaultMessage="This course ends on {endDate}. Final grades and any earned certificates are
scheduled to be available after {certAvailabilityDate}."
description="This shown for leaner when they are eligible for certifcate but it't not available yet, it could because leaners just finished the course quickly!"

View File

@@ -56,11 +56,6 @@ const messages = defineMessages({
defaultMessage: 'Your certificate is available!',
description: 'Header text when the certifcate is available',
},
downloadableBody: {
id: 'progress.certificateStatus.downloadableBody',
defaultMessage: 'Showcase your accomplishment on LinkedIn or your resumé today. You can download your certificate now and access it any time from your Dashboard and Profile.',
description: 'Recommending an action for learner when course certificate is available',
},
viewableButton: {
id: 'progress.certificateStatus.viewableButton',
defaultMessage: 'View my certificate',

View File

@@ -2,7 +2,7 @@ import React, { useState } from 'react';
import PropTypes from 'prop-types';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { OverlayTrigger, Popover } from '@edx/paragon';
import { OverlayTrigger, Popover } from '@openedx/paragon';
import messages from './messages';
@@ -33,7 +33,7 @@ const CompleteDonutSegment = ({ completePercentage, intl, lockedPercentage }) =>
show={showCompletePopover}
placement="top"
overlay={(
<Popover aria-hidden="true">
<Popover id="complete-content-tooltip-popover" aria-hidden="true">
<Popover.Content>
{intl.formatMessage(messages.completeContentTooltip)}
</Popover.Content>

View File

@@ -2,7 +2,7 @@ import React, { useState } from 'react';
import PropTypes from 'prop-types';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { OverlayTrigger, Popover } from '@edx/paragon';
import { OverlayTrigger, Popover } from '@openedx/paragon';
import messages from './messages';
@@ -37,7 +37,7 @@ const IncompleteDonutSegment = ({ incompletePercentage, intl }) => {
show={showIncompletePopover}
placement="top"
overlay={(
<Popover aria-hidden="true">
<Popover id="incomplete-tooltip-popover" aria-hidden="true">
<Popover.Content>
{intl.formatMessage(messages.incompleteContentTooltip)}
</Popover.Content>

View File

@@ -1,7 +1,7 @@
import React, { useState } from 'react';
import PropTypes from 'prop-types';
import { OverlayTrigger, Popover } from '@edx/paragon';
import { OverlayTrigger, Popover } from '@openedx/paragon';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import messages from './messages';
@@ -36,7 +36,7 @@ const LockedDonutSegment = ({ intl, lockedPercentage }) => {
show={showLockedPopover}
placement="top"
overlay={(
<Popover aria-hidden="true">
<Popover id="locked-tooltip-popover" aria-hidden="true">
<Popover.Content>
{intl.formatMessage(messages.lockedContentTooltip)}
</Popover.Content>

View File

@@ -2,8 +2,8 @@ import React from 'react';
import { useSelector } from 'react-redux';
import { getConfig } from '@edx/frontend-platform';
import { FormattedMessage, injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { CheckCircle, WarningFilled, WatchFilled } from '@edx/paragon/icons';
import { Hyperlink, Icon } from '@edx/paragon';
import { CheckCircle, WarningFilled, WatchFilled } from '@openedx/paragon/icons';
import { Hyperlink, Icon } from '@openedx/paragon';
import { useModel } from '../../../generic/model-store';
import { DashboardLink } from '../../../shared/links';

View File

@@ -3,8 +3,8 @@ import { useSelector } from 'react-redux';
import PropTypes from 'prop-types';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { CheckCircle, WarningFilled } from '@edx/paragon/icons';
import { breakpoints, Icon, useWindowSize } from '@edx/paragon';
import { CheckCircle, WarningFilled } from '@openedx/paragon/icons';
import { breakpoints, Icon, useWindowSize } from '@openedx/paragon';
import { useModel } from '../../../../generic/model-store';
import GradeRangeTooltip from './GradeRangeTooltip';

View File

@@ -4,8 +4,8 @@ import { useSelector } from 'react-redux';
import { sendTrackEvent } from '@edx/frontend-platform/analytics';
import { getAuthenticatedUser } from '@edx/frontend-platform/auth';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { Locked } from '@edx/paragon/icons';
import { Button, Icon } from '@edx/paragon';
import { Locked } from '@openedx/paragon/icons';
import { Button, Icon } from '@openedx/paragon';
import { useModel } from '../../../../generic/model-store';
import messages from '../messages';

View File

@@ -5,7 +5,7 @@ import { useSelector } from 'react-redux';
import {
getLocale, injectIntl, intlShape, isRtl,
} from '@edx/frontend-platform/i18n';
import { OverlayTrigger, Popover } from '@edx/paragon';
import { OverlayTrigger, Popover } from '@openedx/paragon';
import { useModel } from '../../../../generic/model-store';

View File

@@ -36,17 +36,17 @@
}
#passing-grade-tooltip {
background: $success-500;
.arrow::after {
border-top-color: $success-500;
}
background: $success-500;
}
#non-passing-grade-tooltip {
background: $accent-b;
.arrow::after {
border-top-color: $accent-b;
}
background: $accent-b;
}

View File

@@ -3,10 +3,10 @@ import { useSelector } from 'react-redux';
import PropTypes from 'prop-types';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { InfoOutline } from '@edx/paragon/icons';
import { InfoOutline } from '@openedx/paragon/icons';
import {
Icon, IconButton, OverlayTrigger, Popover,
} from '@edx/paragon';
} from '@openedx/paragon';
import { useModel } from '../../../../generic/model-store';
import messages from '../messages';

View File

@@ -4,7 +4,7 @@ import PropTypes from 'prop-types';
import {
getLocale, injectIntl, intlShape, isRtl,
} from '@edx/frontend-platform/i18n';
import { OverlayTrigger, Popover } from '@edx/paragon';
import { OverlayTrigger, Popover } from '@openedx/paragon';
import messages from '../messages';

View File

@@ -4,8 +4,8 @@ import { useSelector } from 'react-redux';
import { sendTrackEvent } from '@edx/frontend-platform/analytics';
import { getAuthenticatedUser } from '@edx/frontend-platform/auth';
import { FormattedMessage, injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { Blocked } from '@edx/paragon/icons';
import { Icon, Hyperlink } from '@edx/paragon';
import { Blocked } from '@openedx/paragon/icons';
import { Icon, Hyperlink } from '@openedx/paragon';
import { useModel } from '../../../../generic/model-store';
import DetailedGradesTable from './DetailedGradesTable';

View File

@@ -4,7 +4,7 @@ import { useSelector } from 'react-redux';
import {
getLocale, injectIntl, intlShape, isRtl,
} from '@edx/frontend-platform/i18n';
import { DataTable } from '@edx/paragon';
import { DataTable } from '@openedx/paragon';
import { useModel } from '../../../../generic/model-store';
import messages from '../messages';

View File

@@ -15,8 +15,9 @@ const ProblemScoreDrawer = ({ intl, problemScores, subsection }) => {
<span id="problem-score-label" className="col-auto p-0">{intl.formatMessage(messages.problemScoreLabel)}</span>
<div className={classNames('col', 'p-0', { 'greyed-out': !subsection.learnerHasAccess })}>
<ul className="list-unstyled row w-100 m-0" aria-labelledby="problem-score-label">
{problemScores.map(problemScore => (
<li className="ml-3">{problemScore.earned}{isLocaleRtl ? '\\' : '/'}{problemScore.possible}</li>
{problemScores.map((problemScore, i) => (
// eslint-disable-next-line react/no-array-index-key
<li key={i} className="ml-3">{problemScore.earned}{isLocaleRtl ? '\\' : '/'}{problemScore.possible}</li>
))}
</ul>
</div>

View File

@@ -5,10 +5,10 @@ import PropTypes from 'prop-types';
import { sendTrackEvent } from '@edx/frontend-platform/analytics';
import { getAuthenticatedUser } from '@edx/frontend-platform/auth';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { Collapsible, Icon, Row } from '@edx/paragon';
import { Collapsible, Icon, Row } from '@openedx/paragon';
import {
ArrowDropDown, ArrowDropUp, Blocked, Info,
} from '@edx/paragon/icons';
} from '@openedx/paragon/icons';
import messages from '../messages';
import { useModel } from '../../../../generic/model-store';

View File

@@ -2,8 +2,8 @@ import React from 'react';
import { useSelector } from 'react-redux';
import PropTypes from 'prop-types';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { Blocked } from '@edx/paragon/icons';
import { Icon } from '@edx/paragon';
import { Blocked } from '@openedx/paragon/icons';
import { Icon } from '@openedx/paragon';
import { useModel } from '../../../../generic/model-store';
import messages from '../messages';

View File

@@ -5,8 +5,8 @@ import PropTypes from 'prop-types';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import {
Icon, IconButton, OverlayTrigger, Popover,
} from '@edx/paragon';
import { Blocked, InfoOutline } from '@edx/paragon/icons';
} from '@openedx/paragon';
import { Blocked, InfoOutline } from '@openedx/paragon/icons';
import messages from '../messages';
import { useModel } from '../../../../generic/model-store';

View File

@@ -5,7 +5,7 @@ import { useSelector } from 'react-redux';
import {
getLocale, injectIntl, intlShape, isRtl,
} from '@edx/frontend-platform/i18n';
import { DataTable } from '@edx/paragon';
import { DataTable } from '@openedx/paragon';
import { useModel } from '../../../../generic/model-store';
import AssignmentTypeCell from './AssignmentTypeCell';

View File

@@ -4,7 +4,7 @@ import { useSelector } from 'react-redux';
import {
getLocale, injectIntl, intlShape, isRtl,
} from '@edx/frontend-platform/i18n';
import { DataTable } from '@edx/paragon';
import { DataTable } from '@openedx/paragon';
import { useModel } from '../../../../generic/model-store';
import messages from '../messages';

View File

@@ -4,7 +4,7 @@ import { useSelector } from 'react-redux';
import { sendTrackEvent } from '@edx/frontend-platform/analytics';
import { getAuthenticatedUser } from '@edx/frontend-platform/auth';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { Hyperlink } from '@edx/paragon';
import { Hyperlink } from '@openedx/paragon';
import messages from './messages';
import { useModel } from '../../../generic/model-store';

View File

@@ -8,7 +8,7 @@ import {
Button,
Row,
Col,
} from '@edx/paragon';
} from '@openedx/paragon';
import { resetDeadlines } from '../data';
import { useModel } from '../../generic/model-store';

View File

@@ -7,7 +7,7 @@ import {
Button,
Col,
Row,
} from '@edx/paragon';
} from '@openedx/paragon';
import { useModel } from '../../generic/model-store';
import messages from './messages';

View File

@@ -8,7 +8,7 @@ import {
Button,
Row,
Col,
} from '@edx/paragon';
} from '@openedx/paragon';
import { useModel } from '../../generic/model-store';
import messages from './messages';

View File

@@ -16,23 +16,27 @@ const CourseTabsNavigation = ({
return (
<div id="courseTabsNavigation" className={classNames('course-tabs-navigation', className)}>
<div className="container-xl">
<Tabs
className="nav-underline-tabs"
aria-label={intl.formatMessage(messages.courseMaterial)}
>
{tabs.map(({ url, title, slug }) => (
<a
key={slug}
className={classNames('nav-item flex-shrink-0 nav-link', { active: slug === activeTabSlug })}
href={url}
<div className="nav-bar">
<div className="nav-menu">
<Tabs
className="nav-underline-tabs"
aria-label={intl.formatMessage(messages.courseMaterial)}
>
{title}
</a>
))}
</Tabs>
</div>
<div className="course-tabs-navigation__search-toggle">
<CoursewareSearchToggle />
{tabs.map(({ url, title, slug }) => (
<a
key={slug}
className={classNames('nav-item flex-shrink-0 nav-link', { active: slug === activeTabSlug })}
href={url}
>
{title}
</a>
))}
</Tabs>
</div>
<div className="search-toggle">
<CoursewareSearchToggle />
</div>
</div>
</div>
{show && <CoursewareSearch />}
</div>

View File

@@ -16,9 +16,23 @@
}
}
&__search-toggle {
position: absolute;
top: .05rem;
right: 0;
.nav-bar {
display: flex;
justify-content: space-between;
flex-wrap: wrap;
gap: 10px;
}
.nav-menu {
flex: 1;
}
.search-toggle {
flex-grow: 0;
text-align: right;
white-space: nowrap;
margin-bottom: 10px;
}
}

View File

@@ -88,6 +88,7 @@ describe('CoursewareContainer', () => {
<Routes>
{DECODE_ROUTES.COURSEWARE.map((route) => (
<Route
key={route}
path={route}
element={<CoursewareContainer />}
/>

View File

@@ -1,24 +1,23 @@
import React, { useEffect, useState } from 'react';
import { useEffect, useState } from 'react';
import PropTypes from 'prop-types';
import { Helmet } from 'react-helmet';
import { useDispatch } from 'react-redux';
import { useDispatch, useSelector } from 'react-redux';
import { getConfig } from '@edx/frontend-platform';
import { breakpoints, useWindowSize } from '@edx/paragon';
import { breakpoints, useWindowSize } from '@openedx/paragon';
import { AlertList } from '../../generic/user-messages';
import Sequence from './sequence';
import { CelebrationModal, shouldCelebrateOnSectionLoad, WeeklyGoalCelebrationModal } from './celebration';
import { AlertList } from '@src/generic/user-messages';
import { useModel } from '@src/generic/model-store';
import { getCoursewareOutlineSidebarSettings } from '../data/selectors';
import { Trigger as CourseOutlineTrigger } from './sidebar/sidebars/course-outline';
import Chat from './chat/Chat';
import ContentTools from './content-tools';
import CourseBreadcrumbs from './CourseBreadcrumbs';
import SidebarProvider from './sidebar/SidebarContextProvider';
import SidebarTriggers from './sidebar/SidebarTriggers';
import NewSidebarProvider from './new-sidebar/SidebarContextProvider';
import NewSidebarTriggers from './new-sidebar/SidebarTriggers';
import { useModel } from '../../generic/model-store';
import { CelebrationModal, shouldCelebrateOnSectionLoad, WeeklyGoalCelebrationModal } from './celebration';
import CourseBreadcrumbs from './CourseBreadcrumbs';
import ContentTools from './content-tools';
import Sequence from './sequence';
const Course = ({
courseId,
@@ -33,10 +32,12 @@ const Course = ({
const {
celebrations,
isStaff,
isNewDiscussionSidebarViewEnabled,
} = useModel('courseHomeMeta', courseId);
const sequence = useModel('sequences', sequenceId);
const section = useModel('sections', sequence ? sequence.sectionId : null);
const enableNewSidebar = getConfig().ENABLE_NEW_SIDEBAR;
const { enableNavigationSidebar } = useSelector(getCoursewareOutlineSidebarSettings);
const navigationDisabled = enableNavigationSidebar || (sequence?.navigationDisabled ?? false);
const pageTitleBreadCrumbs = [
sequence,
@@ -53,7 +54,7 @@ const Course = ({
const [weeklyGoalCelebrationOpen, setWeeklyGoalCelebrationOpen] = useState(
celebrations && !celebrations.streakLengthToCelebrate && celebrations.weeklyGoal,
);
const shouldDisplayTriggers = windowWidth >= breakpoints.small.minWidth;
const shouldDisplayChat = windowWidth >= breakpoints.medium.minWidth;
const daysPerWeek = course?.courseGoals?.selectedGoal?.daysPerWeek;
useEffect(() => {
@@ -67,22 +68,26 @@ const Course = ({
));
}, [sequenceId]);
const SidebarProviderComponent = enableNewSidebar === 'true' ? NewSidebarProvider : SidebarProvider;
const SidebarProviderComponent = isNewDiscussionSidebarViewEnabled ? NewSidebarProvider : SidebarProvider;
return (
<SidebarProviderComponent courseId={courseId} unitId={unitId}>
<Helmet>
<title>{`${pageTitleBreadCrumbs.join(' | ')} | ${getConfig().SITE_NAME}`}</title>
</Helmet>
<div className="position-relative d-flex align-items-center mb-4 mt-1">
<CourseBreadcrumbs
courseId={courseId}
sectionId={section ? section.id : null}
sequenceId={sequenceId}
isStaff={isStaff}
unitId={unitId}
/>
{shouldDisplayTriggers && (
<div className="position-relative d-flex align-items-xl-center mb-4 mt-1 flex-column flex-xl-row">
{navigationDisabled || (
<>
<CourseBreadcrumbs
courseId={courseId}
sectionId={section ? section.id : null}
sequenceId={sequenceId}
isStaff={isStaff}
unitId={unitId}
/>
</>
)}
{shouldDisplayChat && (
<>
<Chat
enabled={course.learningAssistantEnabled}
@@ -92,9 +97,12 @@ const Course = ({
contentToolsEnabled={course.showCalculator || course.notes.enabled}
unitId={unitId}
/>
{enableNewSidebar === 'true' ? <NewSidebarTriggers /> : <SidebarTriggers /> }
</>
)}
<div className="w-100 d-flex align-items-center">
<CourseOutlineTrigger isMobileView />
{isNewDiscussionSidebarViewEnabled ? <NewSidebarTriggers /> : <SidebarTriggers /> }
</div>
</div>
<AlertList topic="sequence" />

View File

@@ -1,24 +1,30 @@
import React from 'react';
import { Factory } from 'rosie';
import { getConfig } from '@edx/frontend-platform';
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
import MockAdapter from 'axios-mock-adapter';
import { breakpoints } from '@edx/paragon';
import { breakpoints } from '@openedx/paragon';
import {
act, fireEvent, getByRole, initializeTestStore, loadUnit, render, screen, waitFor,
fireEvent, getByRole, initializeTestStore, loadUnit, render, screen, waitFor,
} from '../../setupTest';
import { buildTopicsFromUnits } from '../data/__factories__/discussionTopics.factory';
import { handleNextSectionCelebration } from './celebration';
import * as celebrationUtils from './celebration/utils';
import { handleNextSectionCelebration } from './celebration';
import Course from './Course';
import { executeThunk } from '../../utils';
import * as thunks from '../data/thunks';
import setupDiscussionSidebar from './test-utils';
jest.mock('@edx/frontend-platform/analytics');
jest.mock('@edx/frontend-lib-special-exams/dist/data/thunks.js', () => ({
...jest.requireActual('@edx/frontend-lib-special-exams/dist/data/thunks.js'),
checkExamEntry: () => jest.fn(),
}));
const mockChatTestId = 'fake-chat';
jest.mock(
'./chat/Chat',
// eslint-disable-next-line react/prop-types
() => function ({ courseId }) {
return <div className="fake-chat" data-testid={mockChatTestId}>Chat contents {courseId} </div>;
},
);
const recordFirstSectionCelebration = jest.fn();
// eslint-disable-next-line no-import-assign
@@ -26,8 +32,6 @@ celebrationUtils.recordFirstSectionCelebration = recordFirstSectionCelebration;
describe('Course', () => {
let store;
let getItemSpy;
let setItemSpy;
const mockData = {
nextSequenceHandler: () => {},
previousSequenceHandler: () => {},
@@ -46,50 +50,48 @@ describe('Course', () => {
global.innerWidth = breakpoints.extraLarge.minWidth;
});
afterAll(() => {
getItemSpy.mockRestore();
setItemSpy.mockRestore();
});
const setupDiscussionSidebar = async () => {
const courseHomeMetadata = Factory.build('courseHomeMetadata', { verified_mode: null });
const testStore = await initializeTestStore({ provider: 'openedx', courseHomeMetadata });
const state = testStore.getState();
const { courseware: { courseId } } = state;
const axiosMock = new MockAdapter(getAuthenticatedHttpClient());
axiosMock.onGet(`${getConfig().LMS_BASE_URL}/api/discussion/v1/courses/${courseId}`).reply(200, { provider: 'openedx' });
const topicsResponse = buildTopicsFromUnits(state.models.units);
axiosMock.onGet(`${getConfig().LMS_BASE_URL}/api/discussion/v2/course_topics/${courseId}`)
.reply(200, topicsResponse);
await executeThunk(thunks.getCourseDiscussionTopics(courseId), testStore.dispatch);
const [firstUnitId] = Object.keys(state.models.units);
mockData.unitId = firstUnitId;
const [firstSequenceId] = Object.keys(state.models.sequences);
mockData.sequenceId = firstSequenceId;
await render(<Course {...mockData} />, { store: testStore, wrapWithRouter: true });
};
it('loads learning sequence', async () => {
render(<Course {...mockData} />, { wrapWithRouter: true });
expect(screen.getByRole('navigation', { name: 'breadcrumb' })).toBeInTheDocument();
expect(await screen.findByText('Loading learning sequence...')).toBeInTheDocument();
expect(screen.queryByRole('navigation', { name: 'breadcrumb' })).not.toBeInTheDocument();
waitFor(() => {
expect(screen.findByText('Loading learning sequence...')).toBeInTheDocument();
expect(screen.queryByRole('alert')).not.toBeInTheDocument();
expect(screen.queryByRole('dialog')).not.toBeInTheDocument();
expect(screen.queryByRole('button', { name: 'Learn About Verified Certificates' })).not.toBeInTheDocument();
expect(screen.queryByRole('alert')).not.toBeInTheDocument();
expect(screen.queryByRole('dialog')).not.toBeInTheDocument();
expect(screen.queryByRole('button', { name: 'Learn About Verified Certificates' })).not.toBeInTheDocument();
loadUnit();
await waitFor(() => expect(screen.queryByText('Loading learning sequence...')).not.toBeInTheDocument());
loadUnit();
expect(screen.queryByText('Loading learning sequence...')).not.toBeInTheDocument();
const { models } = store.getState();
const sequence = models.sequences[mockData.sequenceId];
const section = models.sections[sequence.sectionId];
const course = models.coursewareMeta[mockData.courseId];
expect(document.title).toMatch(
`${sequence.title} | ${section.title} | ${course.title} | edX`,
);
const { models } = store.getState();
const sequence = models.sequences[mockData.sequenceId];
const section = models.sections[sequence.sectionId];
const course = models.coursewareMeta[mockData.courseId];
expect(document.title).toMatch(
`${sequence.title} | ${section.title} | ${course.title} | edX`,
);
});
});
it('removes breadcrumbs when navigation is disabled', async () => {
const sequenceBlocks = [Factory.build(
'block',
{ type: 'sequential', children: [] },
{ courseId: mockData.courseId },
)];
const sequenceMetadata = [Factory.build(
'sequenceMetadata',
{ navigation_disabled: true },
{ courseId: mockData.courseId, sequenceBlock: sequenceBlocks[0] },
)];
const testStore = await initializeTestStore({ sequenceBlocks, sequenceMetadata }, false);
const testData = {
...mockData,
sequenceId: sequenceBlocks[0].id,
onNavigate: jest.fn(),
};
render(<Course {...testData} />, { store: testStore, wrapWithRouter: true });
expect(screen.queryByRole('navigation', { name: 'breadcrumb' })).not.toBeInTheDocument();
});
it('displays first section celebration modal', async () => {
@@ -107,9 +109,11 @@ describe('Course', () => {
handleNextSectionCelebration(sequenceId, sequenceId, testData.unitId);
render(<Course {...testData} />, { store: testStore, wrapWithRouter: true });
const firstSectionCelebrationModal = screen.getByRole('dialog');
expect(firstSectionCelebrationModal).toBeInTheDocument();
expect(getByRole(firstSectionCelebrationModal, 'heading', { name: 'Congratulations!' })).toBeInTheDocument();
waitFor(() => {
const firstSectionCelebrationModal = screen.getByRole('dialog');
expect(firstSectionCelebrationModal).toBeInTheDocument();
expect(getByRole(firstSectionCelebrationModal, 'heading', { name: 'Congratulations!' })).toBeInTheDocument();
});
});
it('displays weekly goal celebration modal', async () => {
@@ -125,37 +129,42 @@ describe('Course', () => {
};
render(<Course {...testData} />, { store: testStore, wrapWithRouter: true });
const weeklyGoalCelebrationModal = screen.getByRole('dialog');
expect(weeklyGoalCelebrationModal).toBeInTheDocument();
expect(getByRole(weeklyGoalCelebrationModal, 'heading', { name: 'You met your goal!' })).toBeInTheDocument();
waitFor(() => {
const weeklyGoalCelebrationModal = screen.getByRole('dialog');
expect(weeklyGoalCelebrationModal).toBeInTheDocument();
expect(getByRole(weeklyGoalCelebrationModal, 'heading', { name: 'You met your goal!' })).toBeInTheDocument();
});
});
it('displays notification trigger and toggles active class on click', async () => {
render(<Course {...mockData} />, { wrapWithRouter: true });
const notificationTrigger = screen.getByRole('button', { name: /Show notification tray/i });
expect(notificationTrigger).toBeInTheDocument();
expect(notificationTrigger.parentNode).not.toHaveClass('mt-3', { exact: true });
fireEvent.click(notificationTrigger);
expect(notificationTrigger.parentNode).toHaveClass('mt-3');
waitFor(() => {
const notificationTrigger = screen.getByRole('button', { name: /Show notification tray/i });
expect(notificationTrigger).toBeInTheDocument();
expect(notificationTrigger.parentNode).not.toHaveClass('sidebar-active', { exact: true });
fireEvent.click(notificationTrigger);
expect(notificationTrigger.parentNode).toHaveClass('sidebar-active');
});
});
it('handles click to open/close discussions sidebar', async () => {
await setupDiscussionSidebar();
const discussionsTrigger = await screen.getByRole('button', { name: /Show discussions tray/i });
const discussionsSideBar = await waitFor(() => screen.findByTestId('sidebar-DISCUSSIONS'));
expect(discussionsSideBar).not.toHaveClass('d-none');
waitFor(() => {
expect(screen.getByTestId('sidebar-DISCUSSIONS')).toBeInTheDocument();
expect(screen.getByTestId('sidebar-DISCUSSIONS')).not.toHaveClass('d-none');
await act(async () => {
const discussionsTrigger = screen.getByRole('button', { name: /Show discussions tray/i });
expect(discussionsTrigger).toBeInTheDocument();
fireEvent.click(discussionsTrigger);
});
await expect(discussionsSideBar).toHaveClass('d-none');
await act(async () => {
expect(screen.queryByTestId('sidebar-DISCUSSIONS')).not.toBeInTheDocument();
fireEvent.click(discussionsTrigger);
expect(screen.queryByTestId('sidebar-DISCUSSIONS')).toBeInTheDocument();
});
await expect(discussionsSideBar).not.toHaveClass('d-none');
});
it('displays discussions sidebar when unit changes', async () => {
@@ -174,20 +183,23 @@ describe('Course', () => {
const { rerender } = render(<Course {...testData} />, { store: testStore });
loadUnit();
await waitFor(() => {
expect(screen.getByTestId('sidebar-DISCUSSIONS')).toBeInTheDocument();
expect(screen.getByTestId('sidebar-DISCUSSIONS')).not.toHaveClass('d-none');
waitFor(() => {
expect(screen.findByTestId('sidebar-DISCUSSIONS')).toBeInTheDocument();
expect(screen.findByTestId('sidebar-DISCUSSIONS')).not.toHaveClass('d-none');
});
rerender(null);
});
it('handles click to open/close notification tray', async () => {
render(<Course {...mockData} />, { wrapWithRouter: true });
const notificationShowButton = await screen.findByRole('button', { name: /Show notification tray/i });
expect(screen.queryByRole('region', { name: /notification tray/i })).toHaveClass('d-none');
fireEvent.click(notificationShowButton);
expect(screen.queryByRole('region', { name: /notification tray/i })).not.toHaveClass('d-none');
await setupDiscussionSidebar();
waitFor(() => {
const notificationShowButton = screen.findByRole('button', { name: /Show notification tray/i });
expect(screen.queryByRole('region', { name: /notification tray/i })).not.toBeInTheDocument();
fireEvent.click(notificationShowButton);
expect(screen.queryByRole('region', { name: /notification tray/i })).toBeInTheDocument();
expect(screen.queryByRole('region', { name: /notification tray/i })).not.toHaveClass('d-none');
});
});
it('renders course breadcrumbs as expected', async () => {
@@ -197,7 +209,9 @@ describe('Course', () => {
{ type: 'vertical' },
{ courseId: courseMetadata.id },
));
const testStore = await initializeTestStore({ courseMetadata, unitBlocks }, false);
const testStore = await initializeTestStore({
courseMetadata, unitBlocks, enableNavigationSidebar: { enable_navigation_sidebar: false },
}, false);
const { courseware, models } = testStore.getState();
const { courseId, sequenceId } = courseware;
const testData = {
@@ -209,10 +223,14 @@ describe('Course', () => {
render(<Course {...testData} />, { store: testStore, wrapWithRouter: true });
loadUnit();
await waitFor(() => expect(screen.queryByText('Loading learning sequence...')).not.toBeInTheDocument());
await waitFor(() => {
expect(screen.queryByText('Loading learning sequence...')).not.toBeInTheDocument();
});
// expect the section and sequence "titles" to be loaded in as breadcrumb labels.
expect(screen.getByText(Object.values(models.sections)[0].title)).toBeInTheDocument();
expect(screen.getByText(Object.values(models.sequences)[0].title)).toBeInTheDocument();
waitFor(() => {
expect(screen.findByText(Object.values(models.sections)[0].title)).toBeInTheDocument();
expect(screen.findByText(Object.values(models.sequences)[0].title)).toBeInTheDocument();
});
});
it('passes handlers to the sequence', async () => {
@@ -241,14 +259,16 @@ describe('Course', () => {
render(<Course {...testData} />, { store: testStore, wrapWithRouter: true });
loadUnit();
await waitFor(() => expect(screen.queryByText('Loading learning sequence...')).not.toBeInTheDocument());
screen.getAllByRole('link', { name: /previous/i }).forEach(link => fireEvent.click(link));
screen.getAllByRole('link', { name: /next/i }).forEach(link => fireEvent.click(link));
waitFor(() => {
expect(screen.queryByText('Loading learning sequence...')).not.toBeInTheDocument();
screen.getAllByRole('link', { name: /previous/i }).forEach(link => fireEvent.click(link));
screen.getAllByRole('link', { name: /next/i }).forEach(link => fireEvent.click(link));
// We are in the middle of the sequence, so no
expect(previousSequenceHandler).not.toHaveBeenCalled();
expect(nextSequenceHandler).not.toHaveBeenCalled();
expect(unitNavigationHandler).toHaveBeenCalledTimes(4);
// We are in the middle of the sequence, so no
expect(previousSequenceHandler).not.toHaveBeenCalled();
expect(nextSequenceHandler).not.toHaveBeenCalled();
expect(unitNavigationHandler).toHaveBeenCalledTimes(4);
});
});
describe('Sequence alerts display', () => {
@@ -268,7 +288,7 @@ describe('Course', () => {
sequenceId: sequenceBlocks[0].id,
};
render(<Course {...testData} />, { store: testStore, wrapWithRouter: true });
await waitFor(() => expect(screen.getByText('Some random banner text to display.')).toBeInTheDocument());
waitFor(() => expect(screen.findByText('Some random banner text to display.')).toBeInTheDocument());
});
it('renders Entrance Exam alert with passing score', async () => {
@@ -302,7 +322,7 @@ describe('Course', () => {
sequenceId: sequenceBlocks[0].id,
};
render(<Course {...testData} />, { store: testStore, wrapWithRouter: true });
await waitFor(() => expect(screen.getByText('Your score is 100%. You have passed the entrance exam.')).toBeInTheDocument());
waitFor(() => expect(screen.findByText('Your score is 100%. You have passed the entrance exam.')).toBeInTheDocument());
});
it('renders Entrance Exam alert with non-passing score', async () => {
@@ -336,7 +356,44 @@ describe('Course', () => {
sequenceId: sequenceBlocks[0].id,
};
render(<Course {...testData} />, { store: testStore, wrapWithRouter: true });
await waitFor(() => expect(screen.getByText('To access course materials, you must score 70% or higher on this exam. Your current score is 30%.')).toBeInTheDocument());
waitFor(() => expect(screen.findByText('To access course materials, you must score 70% or higher on this exam. Your current score is 30%.')).toBeInTheDocument());
});
});
it('displays chat when screen is wide enough (browser)', async () => {
const courseMetadata = Factory.build('courseMetadata', {
learning_assistant_enabled: true,
enrollment: { mode: 'verified' },
});
const testStore = await initializeTestStore({ courseMetadata }, false);
const { courseware } = testStore.getState();
const { courseId, sequenceId } = courseware;
const testData = {
...mockData,
courseId,
sequenceId,
};
render(<Course {...testData} />, { store: testStore, wrapWithRouter: true });
const chat = screen.queryByTestId(mockChatTestId);
waitFor(() => expect(chat).toBeInTheDocument());
});
it('does not display chat when screen is too narrow (mobile)', async () => {
global.innerWidth = breakpoints.extraSmall.minWidth;
const courseMetadata = Factory.build('courseMetadata', {
learning_assistant_enabled: true,
enrollment: { mode: 'verified' },
});
const testStore = await initializeTestStore({ courseMetadata }, false);
const { courseware } = testStore.getState();
const { courseId, sequenceId } = courseware;
const testData = {
...mockData,
courseId,
sequenceId,
};
render(<Course {...testData} />, { store: testStore, wrapWithRouter: true });
const chat = screen.queryByTestId(mockChatTestId);
await expect(chat).not.toBeInTheDocument();
});
});

View File

@@ -5,7 +5,7 @@ import { FormattedMessage } from '@edx/frontend-platform/i18n';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faHome } from '@fortawesome/free-solid-svg-icons';
import { useSelector } from 'react-redux';
import { useToggle, ModalPopup, Menu } from '@edx/paragon';
import { useToggle, ModalPopup, Menu } from '@openedx/paragon';
import { Link } from 'react-router-dom';
import { useModel, useModels } from '../../generic/model-store';
import JumpNavMenuItem from './JumpNavMenuItem';
@@ -62,6 +62,7 @@ const CourseBreadcrumb = ({
<Menu>
{content.map((item) => (
<JumpNavMenuItem
key={item.label}
isDefault={item.default}
sequences={item.sequences}
courseId={courseId}
@@ -153,7 +154,7 @@ const CourseBreadcrumbs = ({
}, [courseStatus, sequenceStatus, allSequencesInSections]);
return (
<nav aria-label="breadcrumb" className="d-inline-block col-sm-10">
<nav aria-label="breadcrumb" className="d-inline-block col-sm-10 mb-3">
<ol className="list-unstyled d-flex flex-nowrap align-items-center m-0">
<li className="list-unstyled col-auto m-0 p-0">
<Link
@@ -169,8 +170,10 @@ const CourseBreadcrumbs = ({
/>
</Link>
</li>
{links.map((content) => (
{links.map((content, i) => (
<CourseBreadcrumb
// eslint-disable-next-line react/no-array-index-key
key={i}
courseId={courseId}
sequenceId={sequenceId}
content={content}

View File

@@ -1,6 +1,6 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Dropdown } from '@edx/paragon';
import { Dropdown } from '@openedx/paragon';
import {
sendTrackingLogEvent,

View File

@@ -1,6 +1,6 @@
import React, { useCallback } from 'react';
import PropTypes from 'prop-types';
import { StatefulButton } from '@edx/paragon';
import { StatefulButton } from '@openedx/paragon';
import { FormattedMessage } from '@edx/frontend-platform/i18n';
import { useDispatch } from 'react-redux';
import BookmarkOutlineIcon from './BookmarkOutlineIcon';

View File

@@ -7,7 +7,7 @@ import {
Button,
StandardModal,
useWindowSize,
} from '@edx/paragon';
} from '@openedx/paragon';
import { useDispatch } from 'react-redux';
import ClapsMobile from './assets/claps_280x201.gif';

View File

@@ -3,8 +3,8 @@ import PropTypes from 'prop-types';
import { FormattedMessage, injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import {
ActionRow, Button, Icon, StandardModal,
} from '@edx/paragon';
import { Lightbulb } from '@edx/paragon/icons';
} from '@openedx/paragon';
import { Lightbulb } from '@openedx/paragon/icons';
import Target from './assets/target.svg';
import messages from './messages';

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