Compare commits

...

140 Commits

Author SHA1 Message Date
Kristin Aoki
d80a68132a feat: bump frontend-lib-content-components to 2.5.1 (#1174)
The primary purpose of this version bump backport is to remove the 2U feedback form
link from the editors (https://github.com/openedx/frontend-lib-content-components/issues/476),
but several other improvements will also be pulled in:

* Fix the Text editor so that when an image is added, it is added at the cursor,
  instead of the beginning of the component.
* Improve editor load time by reducing API calls and switching some calls to be lazy.
* Align controls better in the group feedback component.
* Add validation for start & stop date fields.
* Fix image handling bugs in both the raw & visual text editors.

Full changelog: https://github.com/openedx/frontend-lib-content-components/compare/v2.1.11...v2.5.1

Co-authored-by: Kyle McCormick <kyle@axim.org>
2024-07-23 10:44:07 -04:00
Maria Grimaldi
b66238c7c0 fix: bump frontend-lib-content-components package (#1071) (#1075)
Co-authored-by: Kristin Aoki <42981026+KristinAoki@users.noreply.github.com>
2024-06-06 13:39:44 -04:00
Chris Chávez
e4c5238f70 fix: Bug - Unusable "Languages" taxonomy appears in tagging drawer (#1057)
* Hide language taxonomy when empty
* New message on search result when taxonomy is empty
* Empty taxonomies message added in drawer
2024-06-05 17:30:17 +05:30
Yusuf Musleh
1bc759a1e7 fix: Search result redirect to unit lib component (#1027) (#1069)
This change fixes redirection to the library component in the unit when selecting the search result. It also fixes an issue with navigating to the library MFE when selecting a library component.
2024-06-05 17:19:15 +05:30
Ihor Romaniuk
5cc04f8a80 fix: info icon shrinking on advanced settings page (#1068) 2024-06-03 11:20:35 -04:00
Chris Chávez
de4189b4a5 feat: Show toast when exporting course tags (#1051) 2024-06-03 20:02:53 +05:30
Maria Grimaldi
785b91d3c7 fix: allow grace period minutes only (#1064) (#1067)
* fix: allow grace period minutes only

* fix: zero minutes error

Co-authored-by: Kristin Aoki <42981026+KristinAoki@users.noreply.github.com>
2024-06-03 09:31:26 -04:00
Glib Glugovskiy
a63409eaa6 fix: wrong lock status update message (#1053) (#1054)
Co-authored-by: Kristin Aoki <42981026+KristinAoki@users.noreply.github.com>
2024-05-29 09:46:51 -04:00
Glib Glugovskiy
3c8e5b2501 fix: update date using utc timezone instead of local (#1043) (#1055)
* fix: update date using utc timezone instead of local

* fix: lint error

Co-authored-by: Kristin Aoki <42981026+KristinAoki@users.noreply.github.com>
2024-05-29 09:46:28 -04:00
Peter Kulko
a88066a2c5 fix: fixed course rerun route (#993) 2024-05-09 13:51:22 -03:00
Chris Chávez
e0fb41d8f5 [FC-0049] feat: Other Tags section added on tags drawer (#987)
Adds the new "Other tags" Section to tags drawer that contains the taxonomies/tags that the user doesn't have permission to see/edit. It allow to delete those tags.
2024-05-09 21:34:22 +05:30
Peter Kulko
55adcfe90d feat: Added errors handling 4xx, 5xx (#992)
* feat: [AXIMST-538] Add errors handling 4xx, 5xx

* fix: resolve discussions

* fix: second round of review discussions

refactor: fixing tests for Textbooks page

Co-authored-by: ruzniaievdm <ruzniaievdm@gmail.com>
2024-05-09 11:10:28 -03:00
ruzniaievdm
c884ff2882 fix: Group configurations - Add a counter for usage groups (#991) 2024-05-09 08:59:44 -03:00
Navin Karkera
5c1df3e16e feat: better api error handling (#972)
Improve API error handling.
2024-05-09 11:18:00 +05:30
Yusuf Musleh
8aea28c6e0 feat: search filters refinement (#980)
Sort blocktypes on hierarchy then alphabet.
2024-05-09 11:10:21 +05:30
Braden MacDonald
14245bc6ad feat: Enable taxonomy/tagging feature in MFE by default (#989)
* feat: enable tagging/taxonomy feature by default

* test: improve coverage

* chore: fix lint issue
2024-05-08 18:12:56 -03:00
Chris Chávez
dd9202fafe feat: prevent losing work when users click outside tag drawer after making edits [FC-0036] 2024-05-08 15:56:32 -04:00
Jillian
23fb68f2c3 feat: show "No tags added yet. [Add tags]" on the tag drawer (#988)
when we've expanded a taxonomy in read mode with no content tags added.
2024-05-08 16:47:39 -03:00
Chris Chávez
92b7ae1b77 feat: Add In progress message on import taxonomy (#953)
Message added as a toast
2024-05-08 16:44:21 -03:00
Navin Karkera
087c82c60c refactor: handle relative proctoring link (#974) 2024-05-07 12:01:41 +05:30
Chris Chávez
de408b5a3a [FC-0036] refactor: Further refinements to tag drawer (#970)
* refactor: Further refinements to tag drawer
* Padding to top and left tagging drawer
* Changes in headings in the tagging drawer
2024-05-06 14:46:46 +05:30
Bryann Valderrama
2f5d4f71ec feat: add ENABLE_GRADING_METHOD_IN_PROBLEMS feature flag (#932) 2024-05-03 10:54:52 -04:00
Chris Chávez
64be7e3b37 feat: Send messages after update tags (#975)
Updates the code of Tag Drawer to send two messages to parent (if the drawer is a iframe) when the tags are updated:

- One message with updated content tags.
- One message with the content tags count.
2024-05-03 20:04:42 +05:30
Navin Karkera
a63c808300 refactor: change expand-collapse arrows in outline (#973)
Set arrow down for expanded section/subsection and right arrow for
collapsed items same as legacy
2024-05-03 18:36:51 +05:30
Yusuf Musleh
6d9a8a1eac feat: search modal refinements (#959)
* feat: More spacing between search bar and selectmenu
* feat: Autofocus search field when modal opens
* feat: Fix issues with scroll to search result
This includes the following:
  - The target search element is aligned to the top of the page when scrolling to it
  - Makes sure the section/subsection is expanded in order to scroll to result
* fix: Match focus border radius with button's
* fix: Only expand (sub)section with search result
2024-05-03 15:02:22 +05:30
vladislavkeblysh
65f45f72f0 feat: [FC-0044] Textbooks Page (#890)
Implement Textbooks page.

---------

Co-authored-by: Glib Glugovskiy <glib.glugovskiy@raccoongang.com>
2024-04-30 18:38:43 -03:00
Peter Kulko
a9a73efbb6 feat: [FC-0044] Course unit - Drag and drop for xblocks (#908)
Implements drag and drop for xblocks in the unit page.
2024-04-30 11:21:51 -03:00
Rômulo Penido
e24fb7889e feat: redirect to unit page if the hit or its parent is a unit (#957)
This change adds the feature to redirect from a search result to a Unit, in case the hit parent is a Unit, or the hit is a unit itself.
2024-04-29 19:50:12 +05:30
XnpioChV
9327948b61 feat: [FC-0036] Refined tag drawer
It contains different changes to achieve the reading and editing mode of the drawer tag:

* Manage tags drawer footer with buttons added.
* Creation of ContentTagsDrawerContext.
* Creation of global state and global removed state to allow edit mode.
* Update API client to match with openedx-learning 0.9.1: Save tags of multiple taxonomies; to save all tags added/removed on edit mode
* Extract TagsTree and use it on the Tags Drawer.
* Update TagsTree to allow edit mode.
* Add a Toast on Tags Drawer; show the toast afert save.
* Scrolling + sticky footer on tags drawer
2024-04-25 15:29:13 -04:00
Yusuf Musleh
4146fa6c6e feat: Remove taxonomy export id prompt (#955) 2024-04-26 00:42:32 +05:30
Felipe Montoya
be71668b8d fix: removing ENABLE_NEW_EDITOR_PAGES flag (#951) 2024-04-25 08:35:45 -03:00
Peter Kulko
5686dee43b feat: [FC-0044] Course unit - Copy/paste functionality (#884)
Implement copy/paste.

Co-authored-by: monteri <36768631+monteri@users.noreply.github.com>
Co-authored-by: ihor-romaniuk <ihor.romaniuk@raccoongang.com>
2024-04-24 15:27:29 -03:00
Raymond Zhou
bef6796da4 fix: errors for positive time zones (#961) 2024-04-24 09:07:15 -07:00
Leangseu Kim
e55f031c39 chore: add slot to allow additional course app plugin (#941)
* chore: add @openedx/frontend-plugin-framework

chore: move plugin page setting button to a props

chore: split out app setting modal for reusability

chore: add implementation of WTC plugin

chore: update app setting form

chore: implement the plugin form with mock

chore: follow the UI design

chore: remove translation plugin and move it into frontend-plugin instead

* chore: add eslint ignore for env.config.jsx

* chore: update package-lock.json
2024-04-24 11:55:46 -04:00
connorhaugh
98138181f7 feat: add browser dialog to prevent navigation (#962)
Switching from undefined to "" tells the browser to put in a modal which requires users to confirm thier navigation away. this will prevent continued annoyances from upload failures: https://2u-internal.atlassian.net/browse/TNL-11587
2024-04-24 11:04:12 -04:00
Braden MacDonald
c32462e21e feat: Allow filtering by multiple tags [FC-0040] (#945)
As of #918 , the content search only allows filtering the results by one tag at a time, which is a limitation of Instantsearch.

So with this change, usage of Instantsearch + instant-meilisearch has been replaced with direct usage of Meilisearch. Not only does this simplify the code and make our MFE bundle size smaller, but it allows us much more control over how the tags filtering works, so that we can implement searching by multiple tags.

Trying to modify Instantsearch to do that was too difficult, given the complexity of its codebase.

Related ticket: openedx/modular-learning#201
2024-04-24 09:15:17 +05:30
Kristin Aoki
34104495c5 fix: adding files count in toast (#960)
* fix: adding files count in toast

* fix: toast to use plural function
2024-04-23 16:05:13 -04:00
ruzniaievdm
907ce50071 feat: [FC-0044] group configurations MFE page (#929)
* feat: group configurations - index page

* feat: [AXIMST-63] Index group configurations page

* fix: resolve discussions

* fix: resolve second round discussions

* feat: group configurations - content group actions

* feat: [AXIMST-75, AXIMST-69, AXIMST-81] Content group actions

* fix: resolve conversations

* feat: group configurations - sidebar

* feat: [AXIMST-87] group-configuration page sidebar

* refactor: [AXIMST-87] add changes after review

* refactor: [AXIMST-87] add changes after review

* refactor: [AXIMST-87] add changes ater review

---------

Co-authored-by: Kyrylo Hudym-Levkovych <kyr.hudym@kyrs-MacBook-Pro.local>

* fix: group configurations - the page reloads after the user saves changes

* feat: group configurations - experiment groups

* feat: [AXIMST-93, 99, 105] Group configuration - Experiment Groups

* fix: [AXIMST-518, 537] Group configuration - resolve bugs

* fix: review discussions

* fix: revert classname case

* fix: group configurations - resolve discussions

fix: [AXIMST-714] icon is aligned with text (#210)

* fix: add hook tests

* fix: add thunk tests

* fix: add slice tests

* chore: group configurations - messages

* fix: group configurations - remove delete in edit mode

---------

Co-authored-by: Kyr <40792129+khudym@users.noreply.github.com>
Co-authored-by: Kyrylo Hudym-Levkovych <kyr.hudym@kyrs-MacBook-Pro.local>
Co-authored-by: monteri <lansevermore>
2024-04-23 11:53:49 -04:00
Rômulo Penido
7f668a6ca4 refactor: remove old taxonomy page route (#954) 2024-04-23 14:52:17 +05:30
Ihor Romaniuk
6ec44b5f41 feat: [FC-0044] Unit page - Manage access modal (unit & xblocks) (#901)
* feat: [FC-0044] Unit page - Manage access modal (unit & xblocks)

* fix: add message description
2024-04-22 11:13:16 -04:00
sundasnoreen12
1834655399 fix: fixed scroll issue of provider and settings (#958) 2024-04-22 15:04:52 +05:00
Raymond Zhou
bfcac5c0dd feat: bump flcc (#956) 2024-04-18 15:24:54 -04:00
Chris Chávez
422a5db6f9 [FC-0036] feat: Sort taxonomies (#949)
Taxonomies are now sorted by tag count for those with applied tags, and by name for the rest.
2024-04-18 09:15:33 +05:30
Chris Chávez
08140226c3 [FC-0036] Refined tag drawer UI style (#947)
* feat: Remove 'x' close btn from tags drawer

* feat: Change style for taxonomy tags count

* feat: Update heading styles in tags drawer

* feat: Move dropdown arrows to left of taxonomy

---------

Co-authored-by: Yusuf Musleh <yusuf@opencraft.com>
2024-04-17 19:16:25 +05:30
Rômulo Penido
612d1d8c63 feat: improve search modal results [FC-0040] (#946)
This change makes improvements to the `SearchResult` component which shows the search results in a number of ways:

- It improves the information messages that show up before you start search, and if the results are empty. 
- It adds more context to the search results, by displaying the location of the content
- It adds a link to directly navigate to the relevant content item. 
- It adds an animated highlight to a unit right after you navigate to it. 

---------

Co-authored-by: Jillian <jill@opencraft.com>
Co-authored-by: Braden MacDonald <mail@bradenm.com>
2024-04-17 13:43:56 +05:30
Feanil Patel
b119671ee2 build: Update codecov and start using repo tokens.
Update codecove to the latest version and start using the per repo
tokens which will soon be required to get more reliable coverage builds.

This change also has a corresponding addition of a codecov repo upload
token to the repository secrets for this repo.
2024-04-16 11:55:55 -04:00
Jesper Hodge
0f440c6b3a Fix video upload failures (#952) 2024-04-15 16:49:07 -04:00
Braden MacDonald
2fda48fa5f Bump Paragon to v22.2.1, fix some bugs that turned up (#933)
* chore: bump paragon version

* chore: update snapshot test - alt moved from sr-only to aria-label
2024-04-12 14:05:56 -04:00
Kristin Aoki
63e220ee3e fix: change dropdowns to menus to fix off-screen render (#948) 2024-04-12 13:52:51 -04:00
Jhon Vente
2641aecc8a feat: home studio search filters (#846)
* feat: pagination studio home for courses

* chore: addressing some comments

* refactor: addressing pr comments

* test: adding test for studio home slice

* feat: search input and filters for course home

* fix: using open edx paragon

* feat: usedebounce hook for searching courses

* fix: filters params for searching coruses

* feat: adding coursekey when course name is empty

* chore: remove edit in studio button

* fix: message changed when courses were  not found

* refactor: support courses tab filters and pagination

* test: more cases for course filters component

* refactor: coverage for onsubmit search field

* test: unit test for courses tab component

* feat: loading for search input and layout of course tab

* fix: linter problems

* test: adding more tests for courses tab

* refactor: don't ignore empty string as a case for searching

* refactor: manage empty search bar as special case for searching

* fix: remove expected dispatch mock for clear button

---------

Co-authored-by: Maria Grimaldi <maria.grimaldi@edunext.co>
2024-04-11 16:25:19 -04:00
Braden MacDonald
fc3e38f63b feat: Content Search Modal: Filters [FC-0040] (#918)
Implementation of openedx/modular-learning#201

Implements a modal for searching course content with filters for searching in current or all courses, filtering by content type, content tags and text.
2024-04-11 10:01:06 +05:30
Raymond Zhou
aaf4989610 fix: react datepicker workaround for local time (#944) 2024-04-10 15:57:46 -04:00
Kristin Aoki
fd6b9ae3a6 fix: referencing activeStatus for undefined (#943) 2024-04-10 14:50:40 -04:00
Kristin Aoki
fdcda9833f feat: include usage locations in delete modal (#938) 2024-04-10 15:34:06 +00:00
Jesper Hodge
74eaaa1f9e chore: change github workflow file to not fail pipeline for codecov (#942) 2024-04-10 11:25:08 -04:00
Rômulo Penido
2adff6e51d feat: add content search modal [FC-0040] (#928)
* feat: Prototype search UI using Instantsearch + Meilisearch
---------

Co-authored-by: Braden MacDonald <braden@opencraft.com>
2024-04-08 21:39:15 +05:30
Dmytro
5634e9e507 fix: incorrect redirect link to Pages&Resources from Custom Pages (#916)
Co-authored-by: Dima Alipov <dimaalipov@MacBook-Pro-Dima.local>
2024-04-08 10:03:01 -04:00
Braden MacDonald
ced2c0e891 Fix the TaxonomyMenu test, which wasn't getting run at all (#930)
* test: fix the TaxonomyMenu test, which wasn't getting run at all
Co-authored-by: Rômulo Penido <romulo@opencraft.com>
2024-04-08 13:04:40 +05:30
Rômulo Penido
99a144a869 fix: message case (#924) 2024-04-06 09:00:27 +05:30
Yusuf Musleh
50d2577353 feat: Put Taxonomies tab behind flag (#937) 2024-04-05 12:01:52 -04:00
renovate[bot]
7f3164bbd7 fix(deps): update dependency @edx/frontend-lib-content-components to v2.1.6 (#935)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-04-04 20:24:38 +00:00
dependabot[bot]
3c64eb75aa chore(deps): bump axios from 0.27.2 to 0.28.1 (#936)
Bumps [axios](https://github.com/axios/axios) from 0.27.2 to 0.28.1.
- [Release notes](https://github.com/axios/axios/releases)
- [Changelog](https://github.com/axios/axios/blob/v0.28.1/CHANGELOG.md)
- [Commits](https://github.com/axios/axios/compare/v0.27.2...v0.28.1)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-04-04 20:12:36 +00:00
dependabot[bot]
3c74cd23b2 chore(deps-dev): bump axios from 0.27.2 to 0.28.0 (#848)
Bumps [axios](https://github.com/axios/axios) from 0.27.2 to 0.28.0.
- [Release notes](https://github.com/axios/axios/releases)
- [Changelog](https://github.com/axios/axios/blob/v0.28.0/CHANGELOG.md)
- [Commits](https://github.com/axios/axios/compare/v0.27.2...v0.28.0)

---
updated-dependencies:
- dependency-name: axios
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-04-04 16:00:55 -04:00
Kyr
e306b62dd1 feat: [FC-0044] Certificates page (#872)
* feat: [FC-0044]  Certificates page

* feat: add descriptions for details, signatories, sidebar i18n messages

---------

Co-authored-by: Kyrylo Hudym-Levkovych <kyr.hudym@kyrs-MacBook-Pro.local>
2024-04-04 13:28:04 -04:00
dependabot[bot]
b61cb5c7cd chore(deps): bump express and @openedx/frontend-build (#931)
Bumps [express](https://github.com/expressjs/express) to 4.19.2 and updates ancestor dependency [@openedx/frontend-build](https://github.com/openedx/frontend-build). These dependencies need to be updated together.


Updates `express` from 4.18.2 to 4.19.2
- [Release notes](https://github.com/expressjs/express/releases)
- [Changelog](https://github.com/expressjs/express/blob/master/History.md)
- [Commits](https://github.com/expressjs/express/compare/4.18.2...4.19.2)

Updates `@openedx/frontend-build` from 13.0.27 to 13.1.0
- [Release notes](https://github.com/openedx/frontend-build/releases)
- [Commits](https://github.com/openedx/frontend-build/compare/v13.0.27...v13.1.0)

---
updated-dependencies:
- dependency-name: express
  dependency-type: indirect
- dependency-name: "@openedx/frontend-build"
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-04-04 12:55:10 -04:00
dependabot[bot]
0b1505975b chore(deps): bump follow-redirects from 1.15.5 to 1.15.6 (#902)
Bumps [follow-redirects](https://github.com/follow-redirects/follow-redirects) from 1.15.5 to 1.15.6.
- [Release notes](https://github.com/follow-redirects/follow-redirects/releases)
- [Commits](https://github.com/follow-redirects/follow-redirects/compare/v1.15.5...v1.15.6)

---
updated-dependencies:
- dependency-name: follow-redirects
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-04-04 12:50:41 -04:00
dependabot[bot]
a9d33f612c chore(deps): bump webpack-dev-middleware from 5.3.3 to 5.3.4 (#917)
Bumps [webpack-dev-middleware](https://github.com/webpack/webpack-dev-middleware) from 5.3.3 to 5.3.4.
- [Release notes](https://github.com/webpack/webpack-dev-middleware/releases)
- [Changelog](https://github.com/webpack/webpack-dev-middleware/blob/v5.3.4/CHANGELOG.md)
- [Commits](https://github.com/webpack/webpack-dev-middleware/compare/v5.3.3...v5.3.4)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-04-04 12:42:54 -04:00
Jesper Hodge
57d2fea5fd fix: set feature flag to default to false for pagination (#934) 2024-04-04 13:53:17 +00:00
Ihor Romaniuk
ffa0f14693 feat: [FC-0044] Unit page - display component support label (#913)
* feat: [FC-0044] Unit page - display component support label

* fix: add message description for tooltip of module support
2024-04-04 09:28:39 -04:00
Yusuf Musleh
806591f1cc feat: Add taxonomies tab in home page (#923) 2024-04-04 17:34:13 +05:30
Jhon Vente
fde3872e2e feat: pagination studio home for courses (#825)
This PR adds pagination for the studio home view and makes minor changes to each course card.

NOTE: This needs to be activated by the environment variable ENABLE_HOME_PAGE_COURSE_API_V2 otherwise, it will continue using the old course list

enable this feature flag
new_studio_mfe.use_new_home_page

* feat: pagination studio home for courses

* chore: addressing some comments

* refactor: addressing pr comments

* test: adding test for studio home slice

* chore: deleting unnecessary blank line

* feat: adding feature for pagination

* refactor: change customParams to requestParams

* fix: linter problems

* fix: course home number of 0 courses

* chore: update feature name for pagination

* fix: pagination enabled request and test for tab section added again

* chore: removing cms link in course card items

* chore: addresing some comments

* fix: array dependency for pagination
2024-04-03 11:38:05 -04:00
Ihor Romaniuk
5247ec5022 feat: [FC-0044] Unit page - make xblock edit functional (#912) 2024-04-02 16:15:09 -04:00
Kristin Aoki
dd13ed49aa feat: upgrade frontend-lib-content-components (#927) 2024-04-02 09:35:29 -04:00
Rômulo Penido
0a50bbc9ef fix: content tags drawer width (#910) 2024-04-02 12:40:14 +05:30
Raymond Zhou
d44edb84a0 feat: send content-length in video (#925) 2024-03-28 15:05:08 -04:00
Rômulo Penido
f57d40ea34 feat: tag sections, subsections, and the whole course (FC-0053) (#879)
* feat: tag sections, subsections, and the whole course
* docs: add comments to useContentTagsCount
2024-03-28 17:44:29 +05:30
Kristin Aoki
80bf86992d fix: transcript and thumbnail uploads (#914)
* fix: transcript and thumbnail uploads

* chore: add missing tests
2024-03-25 10:02:09 -04:00
Yusuf Musleh
1dde30a0a2 [FC-0036] feat: Make tags widget keyboard accessible (#900)
Adds the ability to navigate the new "Add Tags" widget using the keyboard, making it fully accessible through the keyboard.
2024-03-21 17:26:22 +05:30
Braden MacDonald
9a6e12bd3b Clean up Taxonomy API files/hooks/queries [FC-0036] (#850)
* chore: rename apiHooks.jsx to apihooks.js

* refactor: consolidate taxonomy API code

* fix: was not invalidating tags after import

* fix: UI was freezing while computing plan for large import files
2024-03-20 09:31:10 +05:30
Kristin Aoki
784a811ff8 Revert "feat: show alerts related files included via paste unit (#847)" (#909)
This reverts commit 071aee5b02.
2024-03-19 13:34:33 -04:00
Navin Karkera
071aee5b02 feat: show alerts related files included via paste unit (#847)
* fix: block highlight and status for unscheduled course

* feat: show alerts related to files after pasting unit

* refactor: rename paste notices and add view files option to alert

* refactor: remove additional visibility state check

* refactor: page alert message
2024-03-19 10:27:18 -04:00
Navin Karkera
da68fb8e9d feat: allow dragging blocks across parents in outline (#859) 2024-03-19 11:25:02 -03:00
Kristin Aoki
972a7f324c feat: upgrade frontend-lib-content-components (#906) 2024-03-18 16:44:07 -04:00
Kristin Aoki
c809dfb2e4 feat: add pr template (#905) 2024-03-18 15:02:08 -04:00
Samir Sabri
1b2c65fae6 feat!: remove Transifex calls for OEP-58 (#627) 2024-03-18 15:01:20 -04:00
connorhaugh
b09729b55e fix: add new videos to the front of the videos list (#904) 2024-03-18 12:58:59 -04:00
connorhaugh
60917c6ab5 Feat: refactor upload thunk, no progress bar pt 2 (#899)
Attempts to fix: https://2u-internal.atlassian.net/browse/TNL-1141
2024-03-18 11:07:13 -04:00
Chris Chávez
d57ecc6779 [FC-0036] Tags Sidebar (#852)
* refactor: Unit sidebar to create the TagsSidebar

* feat: Structure of TagsSidebar and TagsTree

* feat: Adding styles to the TagsTree

* feat: TagsSidebarHeader created

* feat: Add count on TagsSidebarHeader

* test: Tests for new components added

* style: Update tags count with opacity when the count is zero

* refactor: Extract tag count component as generic

* refactor: Transform Sidebar to a wrapper component

---------

Co-authored-by: Rômulo Penido <romulo@opencraft.com>
2024-03-15 21:59:28 +05:30
connorhaugh
6ae9cdac00 Revert "feat: refactor upload thunk, no progress bar (#894)" (#895)
This reverts commit a88a88e9af.
2024-03-13 14:08:18 -04:00
Kristin Aoki
9740974bbd feat: add duplicate file validation for asset upload (#885)
* feat: add duplicate file validation for asset upload

* fix: modal only appearing once

* feat: add tests for overwrite modal

* fix: input not allowing second upload of same file

* fix: default pageSize for asset details
2024-03-13 12:09:00 -04:00
connorhaugh
a88a88e9af feat: refactor upload thunk, no progress bar (#894)
* feat: add progress bar for video uploads and refactor

---------

Co-authored-by: Kristin Aoki <42981026+KristinAoki@users.noreply.github.com>
2024-03-13 11:50:23 -04:00
connorhaugh
6baec5b6a3 Revert "feat: add progress bar for video uploads and refactor (#860)" (#893)
This reverts commit d76aaa73a4.
2024-03-13 10:18:05 -04:00
Peter Kulko
8acd27d7bf fix: replaced the LMS endpoint for navigating the course unit page
fix: [AXIMST-424] Course unit - Fixed network connection behavior (#138)

* fix: [AXIMST-424] fixed network connetcion behavior

* fix: added placeholder for unsuccessful loading for the page

* refactor: code refactoring
2024-03-13 10:56:10 -03:00
connorhaugh
d76aaa73a4 feat: add progress bar for video uploads and refactor (#860)
* feat: add progress bar for video uploads and refactor

---------

Co-authored-by: Kristin Aoki <42981026+KristinAoki@users.noreply.github.com>
2024-03-13 09:37:55 -04:00
Yusuf Musleh
4e70813fa9 [FC-0036] feat: New "Add Tags" widget (#834)
* feat: Use react-select for tags selector

Replace existing component with react-select component, by passing in
our custom component.
This retained the existing search functionality.

* fix: Fix missing deps causing constant rerender

This bug appeared after removing the react-query call to the backend
when selecting/unselecting a tag in the dropdown. Since it no longer
gets the updated state from the backend, it doesnt mask the bug.

The bug is essentially the `ContentTagsCollapsibleHelper` rerendering
causing the states to reset overriding the selected (not commited) tags.
This is due to missing dependancies in the useCallback.

* feat: Add stagedContentTags state in react-select

This adds a state and callbacks in the toplevel component of the content
tags drawer to be able to add/remove staged content tags and have them
showup in the react-select as selected chips.

* feat: Split up applied & staged content tags trees

Now content tags have seperate tree states for applied ones and staged
ones. They are updated seperately and both are used when updating the
selectable box UI. This allows for more flexibility with actions that
can be performed on the staged content tags with impacting the applied
ones.

* feat: Change style of implicit checkbox to checks

This overrides the indeterminate input checkbox style to match the
checked checkbox style, using variables defined in paragon.

* feat: Add bottom buttons in tags dropdown selector

* refactor: Remove cloneDeep + simplify code

* feat: Update placeholder/button texts

* feat: Implement cancel button + add proptypes

* feat: Implement commit/cancel staged tags

This implements the commit functionality for staged tags, taking account
for implicit tags. This also handles the case for removing applied tags
by clicking on the "x" in the TagBubble.

* feat: Keep all staged tags only commit explicit

* feat: Change style of add/cancel/load more buttons

* feat: Add inline "Add" button to commit tags

In the react-select component, an inline "Add" button showsup when some
tags are staged, if they are clicked they are commited/applied.

* fix: Keep applied tag checked when only staged child unchecked

* feat: Style add tags widget + staged tags

Also clear search term whenever tags are staged/cancelled

* feat: Fixed some typing errors

* test: Update tests to fix existing broken cases

* test: Add new functionality tests

* chore: add types to ContentTagsCollapsible

* chore: add types for useContentTagsCollapsibleHelper

* fix: Small bug with useIntl

* chore: Fix more linter issues

* refactor: Separate stagedTags and stagedTagsTree state updates

This refactor removed the warning that was caused because the state of a
parent component (ContentTagsDrawer) was being updated in the middle of
a state update in (ContentTagsCollapsible). This seperated the two state
updates to avoid this issue.

* chore: Update package-lock.json

* fix: Reset applied tags in selectbox when fetching

Whenever we get new applied tags from the backend, we reset the applied
tags that are checked, and only check the explicit tags. This was
causing an issue of duplicate applied tags being added to the selectbox.

* chore: Update package.json

---------

Co-authored-by: Braden MacDonald <braden@opencraft.com>
2024-03-13 18:57:30 +05:30
Maria Grimaldi
7f5687f175 refactor: address PR reviews 2024-03-12 16:42:11 -03:00
Maria Grimaldi
c8434b87c0 fix: import grouptypes from correct file 2024-03-12 16:42:11 -03:00
Maria Grimaldi
9299f4cf93 fix: import grouptypes from correct file 2024-03-12 16:42:11 -03:00
Maria Grimaldi
59d2dcaacb refactor!: put open manage team behind a configuration flag 2024-03-12 16:42:11 -03:00
Cristhian Garcia
42f8c3d95f chore: fix typo 2024-03-12 16:42:11 -03:00
Cristhian Garcia
9ff77945e3 chore: update open managed description 2024-03-12 16:42:11 -03:00
Cristhian Garcia
584823b879 fix: reorder groups 2024-03-12 16:42:11 -03:00
Cristhian Garcia
6e83e90cf0 feat: add open managed group type 2024-03-12 16:42:11 -03:00
Jeremy Ristau
ad7ba2f302 chore: update CODEOWNERS with new team (#888) 2024-03-12 11:57:23 -04:00
Ihor Romaniuk
bec59e5bbe feat: [FC-0044] Unit page - display xblock components (#857) 2024-03-12 11:48:11 -04:00
Jesper Hodge
1fdddfb869 Fix replace broken selectable box component everywhere (#887)
The Configure Live modal in Pages & Resources page uses a selectable box to select the video conferencing tool. It seems broken as well (not selectable).

It looks like the bug with not working SelectableBox (see e.g. #886) affects pretty much any component that uses it.

Thus, this PR replaces every usage of the paragon component with our working copy from flcc.
2024-03-11 17:10:33 -04:00
Jesper Hodge
b5a287639d Fix SelectableBox problems (#886)
Due to a bug in the SelectableBox component, selecting values was not possible in different components throughout this MFE.

This fixes the Gallery and the Select Problem Types components by updating the FLCC version and replacing the SelectableBox copy with an import from FLCC.

For a full bug description see #880.
2024-03-11 17:09:58 -04:00
renovate[bot]
dad4bd5282 fix(deps): update dependency @edx/frontend-component-footer to v13.0.4 (#881)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-03-11 10:25:27 -04:00
Jesper Hodge
17b1360c07 Fix: SortAndFilter modal doesnt respond to click (#880)
Description
We are encountering a bug in our stage environment that is very hard to reproduce locally, but not impossible. This is the same bug dealt with in several previous PRs like for example #871 (here I'm working on another component that uses the same paragon component and therefore encounters the same bug).

Since I was able to reproduce it locally, it is definitely not just a bug affecting only 2U-specific things.

Expected behavior
open a course with files
select a different sorting order (for example oldest to newest)
you should be able to select the different option
you should be able to successfully apply it
Actual behavior on stage environment
you can't select different option
you can't apply different option
Previous steps
Previously, I reproduced this locally by just adding the latest version of SelectableBox as a copy into this repo and importing it from there. Then, under the mistaken assumption that there was a missing context provider, I added that and it got fixed locally. However it turned out to not work on stage.

Measures taken in this PR
I replaced the entire SelectableBox component with all subcomponents with a version from @edx/paragon@21.5.6, which was apparently the version in the commit that didn't have the error yet. In theory, this would just fix the problem, though I have my doubts. But it's worth a try. I only imported it in one place, in this SortAndFilter modal.
I added console logging for onChange events which seem to the problem, as they are not triggering a change in the value of the context on stage. I see little choice than to log them to get more info. This will only affect the component that's not working.
2024-03-08 17:44:03 -05:00
Chris Chávez
c39b52a6bf [UU-58] Implement tagging & taxonomy feature in outline (#855)
* feat: TagCount component

* feat: Update ContentTagsDrawer to use it in the MFE

* feat: Manage tags menu added on units

* feat: Tag count added on unit

* feat: Add button feat to Tag count

* test: Course Outline api tests

* test: Ignore lines that can not be tested

* style: Comment added on ContentTagsDrawer

* style: Nits on CardHeader
2024-03-08 10:00:51 -05:00
PKulkoRaccoonGang
642b4e4052 refactor: refactoring after review 2024-03-08 08:17:54 -03:00
PKulkoRaccoonGang
423a3f3f72 fix: [AXIMST-470] fixed sidebar status after deleting or duplicating xblock 2024-03-08 08:17:54 -03:00
PKulkoRaccoonGang
f1f036576e refactor: after rebase 2024-03-08 08:17:54 -03:00
PKulkoRaccoonGang
deb76a0609 fix: [AXIMST-473] fixed sidebar publish status 2024-03-08 08:17:54 -03:00
PKulkoRaccoonGang
912c42e802 fix: [AXIMST-472] fixed sidebar visibility notification 2024-03-08 08:17:54 -03:00
PKulkoRaccoonGang
bdd641225f fix: [AXIMST-427] fixed unpublished changes alert 2024-03-08 08:17:54 -03:00
Peter Kulko
9021fccdb7 feat: [AXIMST-25] Course unit - Alert notification about unpublished changes (#135)
* feat: [AXIMST-25] added alert notification about unpublished changes

* feat: added tests

* feat: added translations
2024-03-08 08:17:54 -03:00
Peter Kulko
e4d88fb1fa feat: [AXIMST-24] Course unit - Sidebar buttons functional (#134)
* feat: [AXIMST-24] sidebar buttons functional

* refactor: removed modal extra className

* refactor: refactoring after review
2024-03-08 08:17:54 -03:00
Peter Kulko
073e191273 feat: [AXIMST-23] Course unit - Sidebar with unit info (#117)
* feat: added Sidebar with unit info

* feat: added unit location

* refactor: added legacy behavior

* feat: added live variant

* refactor: code refactoring

* feat: added tests and translations

* feat: added new font size

* refactor: after review
2024-03-08 08:17:54 -03:00
Ihor Romaniuk
f8095e6670 feat: [FC-0044] Unit page - display xblock components 2024-03-08 08:17:54 -03:00
Kristin Aoki
e4c3997d17 fix: revert addition of course-ingestion-tool submodule (#878) 2024-03-07 16:42:46 -05:00
Kristin Aoki
da7fe95f24 fix: accessibility page config reference (#875) 2024-03-07 16:11:04 -05:00
Jesper Hodge
896969c7de Fix radio context provider missing (#874)
This is a temporary fix for a bug that stems from a paragon component, SelectableBox. So we're using our own copy from fronten-lib-content-components.
2024-03-07 15:55:30 -05:00
Kristin Aoki
8100281fb4 feat: add checklist page (#870)
* feat: add checklist page

* fix: failing tests

* fix: styling bugs

* fix: lint errors

* feat: add test fro CourseChecklist

* fix: lint errors

* feat: add ChecklistSection tests

* fix: lint error

* fix: missing api reply status
2024-03-07 15:20:33 -05:00
Jesper Hodge
f035391c2f fix: replace paragon radio select set with copy to debug (#871)
step 1 for trying fixes for the stage bug where the paragon radio select is not clickable. Here I just replace the paragon component with our identical copy to see what that changes. Followup steps are to change this component until hopefully the problem gets fixed.
2024-03-06 16:26:17 -05:00
Eugene Dyudyunov
3607e6423d fix: correct internal routing
The Content dropdown items have incorrect URLs for the
internal routing when MFEs are deployed using the common
domain and the PUBLIC_PATH.
2024-03-06 10:00:35 -03:00
Jesper Hodge
4395607074 chore: update frontend-lib-content-components to 2.1.0 to fix problem select (#867) 2024-03-05 13:03:07 -05:00
Kristin Aoki
f717cdac86 feat: add accessibility page (#861)
* feat: add accessibility page

* fix: lint errors

* feat: increase code coverage

* fix: lint errors
2024-03-05 12:15:58 -05:00
Jeremy Ristau
40c9d6ee0d chore: update tnl team name (#862) 2024-03-05 09:19:39 -05:00
Braden MacDonald
3c661e15cb Convert "Pages & Resources" page to a plugin system (#638)
* feat: Make "Pages & Resources" course apps into plugins

* feat: move ora_settings

* feat: move proctoring

* feat: move progress

* feat: move teams

* feat: move wiki

* feat: move Xpert settings

* fix: add webpack.prod.config.js

* fix: clean up unused parts of package.json files

* feat: Add an error message when displaying a Course App Plugin fails

* chore: fix various eslint warnings

* chore: fix jest tests

* fix: error preventing "npm ci" from working

* feat: better tests for <SettingsComponent>

* chore: move xpert_unit_summary into same dir as other plugins

* fix: eslint-import-resolver-webpack is a dev dependency

* chore: move learning_assistant to be a plugin too

* feat: for compatibility, install 2U plugins by default

* fix: bug with learning_assistant package.json
2024-02-28 11:50:54 -05:00
PKulkoRaccoonGang
49fce4622c refactor: tests refactoring 2024-02-27 14:50:06 -03:00
Chris Chávez
608b2f79f8 [FC-0036] Refined taxonomy details page (#833)
* UX refinements on tag list table
* Add page size to tag list table
* fix Datatable pagination
2024-02-27 21:02:06 +05:30
PKulkoRaccoonGang
6b57ce3e53 refactor: refactoring after review 2024-02-27 11:44:42 -03:00
Peter Kulko
6aff1c1168 feat: [AXIMST-19, 20, 22] Course unit - Modal windows for course unit page components (#118)
* feat: added modal windows for course unit page components

* refactor: code refactoring

* refactor: added translations

* refactor: refactoring after review

* refactor: after review
2024-02-27 11:44:42 -03:00
Ihor Romaniuk
2b11df9eb5 fix: [AXIMST-371] fix correct internal route on create new unit (#114) 2024-02-27 11:44:42 -03:00
Peter Kulko
7fcc501d2e feat: Unit creation button logic and refactoring 2024-02-27 11:44:42 -03:00
Jeremy Ristau
90fb3d8edc chore: add missing maintainership files (#840)
* chore: add catalog-info file for Open edX Backstage

* chore: Create CODEOWNERS
2024-02-27 06:41:21 -05:00
Rômulo Penido
0fc0ce0829 feat: add export course tags menu (#830)
This change adds an item in the Tools menu to export the course tags to a CSV file.
2024-02-21 12:50:40 +05:30
Braden MacDonald
16d2f38325 Display full descendant count on taxonomy tag list page [FC-0036] (#826) 2024-02-20 15:40:30 +05:30
Brian Smith
76bb8e88c1 chore(deps): update paragon and frontend-build to openedx scope 2024-02-16 13:40:03 -03:00
807 changed files with 44191 additions and 31632 deletions

7
.env
View File

@@ -30,13 +30,16 @@ USER_INFO_COOKIE_NAME=''
ENABLE_ACCESSIBILITY_PAGE=false
ENABLE_PROGRESS_GRAPH_SETTINGS=false
ENABLE_TEAM_TYPE_SETTING=false
ENABLE_NEW_EDITOR_PAGES=true
ENABLE_UNIT_PAGE=false
ENABLE_ASSETS_PAGE=false
ENABLE_VIDEO_UPLOAD_PAGE_LINK_IN_CONTENT_DROPDOWN=false
ENABLE_TAGGING_TAXONOMY_PAGES=false
ENABLE_TAGGING_TAXONOMY_PAGES=true
BBB_LEARN_MORE_URL=''
HOTJAR_APP_ID=''
HOTJAR_VERSION=6
HOTJAR_DEBUG=false
INVITE_STUDENTS_EMAIL_TO=''
AI_TRANSLATIONS_BASE_URL=''
ENABLE_HOME_PAGE_COURSE_API_V2=false
ENABLE_CHECKLIST_QUALITY=''
ENABLE_GRADING_METHOD_IN_PROBLEMS=false

View File

@@ -32,9 +32,10 @@ USER_INFO_COOKIE_NAME='edx-user-info'
ENABLE_ACCESSIBILITY_PAGE=false
ENABLE_PROGRESS_GRAPH_SETTINGS=false
ENABLE_TEAM_TYPE_SETTING=false
ENABLE_NEW_EDITOR_PAGES=true
ENABLE_UNIT_PAGE=false
ENABLE_VIDEO_UPLOAD_PAGE_LINK_IN_CONTENT_DROPDOWN=false
ENABLE_ASSETS_PAGE=false
ENABLE_VIDEO_UPLOAD_PAGE_LINK_IN_CONTENT_DROPDOWN=true
ENABLE_NEW_VIDEO_UPLOAD_PAGE=true
ENABLE_TAGGING_TAXONOMY_PAGES=true
BBB_LEARN_MORE_URL=''
HOTJAR_APP_ID=''
@@ -42,3 +43,6 @@ HOTJAR_VERSION=6
HOTJAR_DEBUG=true
INVITE_STUDENTS_EMAIL_TO="someone@domain.com"
AI_TRANSLATIONS_BASE_URL='http://localhost:18760'
ENABLE_HOME_PAGE_COURSE_API_V2=false
ENABLE_CHECKLIST_QUALITY=true
ENABLE_GRADING_METHOD_IN_PROBLEMS=false

View File

@@ -28,9 +28,12 @@ SUPPORT_URL='https://support.edx.org'
USER_INFO_COOKIE_NAME='edx-user-info'
ENABLE_PROGRESS_GRAPH_SETTINGS=false
ENABLE_TEAM_TYPE_SETTING=false
ENABLE_NEW_EDITOR_PAGES=true
ENABLE_UNIT_PAGE=true
ENABLE_ASSETS_PAGE=false
ENABLE_VIDEO_UPLOAD_PAGE_LINK_IN_CONTENT_DROPDOWN=true
ENABLE_TAGGING_TAXONOMY_PAGES=true
BBB_LEARN_MORE_URL=''
INVITE_STUDENTS_EMAIL_TO="someone@domain.com"
ENABLE_HOME_PAGE_COURSE_API_V2=true
ENABLE_CHECKLIST_QUALITY=true
ENABLE_GRADING_METHOD_IN_PROBLEMS=false

View File

@@ -1,4 +1,6 @@
coverage/*
dist/
node_modules/
jest.config.js
jest.config.js
env.config.jsx
example.env.config.jsx

View File

@@ -1,5 +1,6 @@
const path = require('path');
// eslint-disable-next-line import/no-extraneous-dependencies
const { createConfig } = require('@edx/frontend-build');
const { createConfig } = require('@openedx/frontend-build');
module.exports = createConfig(
'eslint',
@@ -13,5 +14,21 @@ module.exports = createConfig(
indent: ['error', 2],
'no-restricted-exports': 'off',
},
settings: {
// Import URLs should be resolved using aliases
'import/resolver': {
webpack: {
config: path.resolve(__dirname, 'webpack.dev.config.js'),
},
},
},
overrides: [
{
files: ['plugins/**/*.test.jsx'],
rules: {
'import/no-extraneous-dependencies': 'off',
},
},
],
},
);

27
.github/pull_request_template.md vendored Normal file
View File

@@ -0,0 +1,27 @@
## Description
Describe what this pull request changes, and why. Include implications for people using this change.
Design decisions and their rationales should be documented in the repo (docstring / ADR), per
Useful information to include:
- Which edX user roles will this change impact? Common user roles are "Learner", "Course Author",
"Developer", and "Operator".
- Include screenshots for changes to the UI (ideally, both "before" and "after" screenshots, if applicable).
- Provide links to the description of corresponding configuration changes. Remember to correctly annotate these
changes.
## Supporting information
Link to other information about the change, such as Jira issues, GitHub issues, or Discourse discussions.
Be sure to check they are publicly readable, or if not, repeat the information here.
## Testing instructions
Please provide detailed step-by-step instructions for testing this change.
## Other information
Include anything else that will help reviewers and consumers understand the change.
- Does this change depend on other changes elsewhere?
- Any special concerns or limitations? For example: deprecations, migrations, security, or accessibility.

View File

@@ -18,6 +18,7 @@ jobs:
node-version: ${{ env.NODE_VER }}
- run: make validate.ci
- name: Upload coverage
uses: codecov/codecov-action@v3
uses: codecov/codecov-action@v4
with:
fail_ci_if_error: true
token: ${{ secrets.CODECOV_TOKEN }}

6
.gitignore vendored
View File

@@ -23,3 +23,9 @@ temp/babel-plugin-react-intl
# Local environment overrides
.env.private
# Messages .json files fetched by atlas
src/i18n/messages/
# environment js config
env.config.jsx

View File

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

2
CODEOWNERS Normal file
View File

@@ -0,0 +1,2 @@
# The following users are the maintainers of all frontend-app-course-authoring files
* @openedx/2u-tnl

View File

@@ -1,7 +1,3 @@
transifex_resource = frontend-app-course-authoring
export TRANSIFEX_RESOURCE = ${transifex_resource}
transifex_langs = "ar,de,de_DE,es_419,fa_IR,fr,fr_CA,hi,it,it_IT,pt,pt_PT,ru,uk,zh_CN"
intl_imports = ./node_modules/.bin/intl-imports.js
transifex_utils = ./node_modules/.bin/transifex-utils.js
i18n = ./src/i18n
@@ -33,23 +29,6 @@ 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 -t -f --mode reviewed --languages=$(transifex_langs)
else
# Pulls translations using atlas.
pull_translations:
rm -rf src/i18n/messages
mkdir src/i18n/messages
@@ -63,7 +42,6 @@ pull_translations:
translations/frontend-app-course-authoring/src/i18n/messages:frontend-app-course-authoring
$(intl_imports) frontend-component-ai-translations frontend-lib-content-components frontend-platform paragon frontend-component-footer frontend-app-course-authoring
endif
# This target is used by Travis.
validate-no-uncommitted-package-lock-changes:

View File

@@ -140,13 +140,6 @@ Requirements
* ``new_core_editors.use_new_video_editor``: must be enabled for the new Video Xblock editor to be used in Studio
* ``new_core_editors.use_new_problem_editor``: must be enabled for the new Problem Xblock editor to be used in Studio
Configuration
-------------
In additional to the standard settings, the following local configuration item is required:
* ``ENABLE_NEW_EDITOR_PAGES``: must be enabled in order to actually present the new XBlock editors (on by default)
Feature Description
-------------------
@@ -267,7 +260,8 @@ Configuration
In additional to the standard settings, the following local configuration items are required:
* ``ENABLE_TAGGING_TAXONOMY_PAGES``: must be enabled in order to actually present the new Tagging/Taxonomy pages.
* ``ENABLE_TAGGING_TAXONOMY_PAGES``: must be enabled (which it is by default) in order to actually enable/show the new
Tagging/Taxonomy functionality.
Developing

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-course-authoring'
description: "The frontend (MFE) for Open edX Course Authoring (aka Studio)"
links:
- url: "https://github.com/openedx/frontend-app-course-authoring"
title: "Frontend app course authoring"
icon: "Web"
annotations:
openedx.org/arch-interest-groups: ""
spec:
owner: group:2u-tnl
type: 'website'
lifecycle: 'production'

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

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

View File

@@ -1,4 +1,4 @@
const { createConfig } = require('@edx/frontend-build');
const { createConfig } = require('@openedx/frontend-build');
module.exports = createConfig('jest', {
setupFilesAfterEnv: [
@@ -11,6 +11,7 @@ module.exports = createConfig('jest', {
],
moduleNameMapper: {
'^lodash-es$': 'lodash',
'^CourseAuthoring/(.*)$': '<rootDir>/src/$1',
},
modulePathIgnorePatterns: [
'/src/pages-and-resources/utils.test.jsx',

19201
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -12,7 +12,7 @@
"scripts": {
"build": "fedx-scripts webpack",
"i18n_extract": "fedx-scripts formatjs extract",
"stylelint": "stylelint \"src/**/*.scss\" \"scss/**/*.scss\" --config .stylelintrc.json",
"stylelint": "stylelint \"plugins/**/*.scss\" \"src/**/*.scss\" \"scss/**/*.scss\" --config .stylelintrc.json",
"lint": "npm run stylelint && fedx-scripts eslint --ext .js --ext .jsx .",
"lint:fix": "npm run stylelint && fedx-scripts eslint --ext .js --ext .jsx . --fix",
"snapshot": "TZ=UTC fedx-scripts jest --updateSnapshot",
@@ -36,24 +36,38 @@
"url": "https://github.com/openedx/frontend-app-course-authoring/issues"
},
"dependencies": {
"@dnd-kit/core": "^6.1.0",
"@dnd-kit/modifiers": "^7.0.0",
"@dnd-kit/sortable": "^8.0.0",
"@dnd-kit/utilities": "^3.2.2",
"@edx/brand": "npm:@openedx/brand-openedx@^1.2.2",
"@edx/frontend-component-ai-translations": "^1.4.0",
"@edx/frontend-component-footer": "^12.3.0",
"@edx/frontend-component-header": "^4.7.0",
"@edx/frontend-component-ai-translations": "^2.0.0",
"@edx/frontend-component-footer": "^13.0.2",
"@edx/frontend-component-header": "^5.1.0",
"@edx/frontend-enterprise-hotjar": "^2.0.0",
"@edx/frontend-lib-content-components": "^1.178.2",
"@edx/frontend-platform": "5.6.1",
"@edx/frontend-lib-content-components": "^2.5.1",
"@edx/frontend-platform": "7.0.1",
"@edx/openedx-atlas": "^0.6.0",
"@edx/paragon": "^21.5.6",
"@fortawesome/fontawesome-svg-core": "1.2.36",
"@fortawesome/free-brands-svg-icons": "5.15.4",
"@fortawesome/free-regular-svg-icons": "5.15.4",
"@fortawesome/free-solid-svg-icons": "5.15.4",
"@fortawesome/react-fontawesome": "0.2.0",
"@meilisearch/instant-meilisearch": "^0.17.0",
"@openedx-plugins/course-app-calculator": "file:plugins/course-apps/calculator",
"@openedx-plugins/course-app-edxnotes": "file:plugins/course-apps/edxnotes",
"@openedx-plugins/course-app-learning_assistant": "file:plugins/course-apps/learning_assistant",
"@openedx-plugins/course-app-live": "file:plugins/course-apps/live",
"@openedx-plugins/course-app-ora_settings": "file:plugins/course-apps/ora_settings",
"@openedx-plugins/course-app-proctoring": "file:plugins/course-apps/proctoring",
"@openedx-plugins/course-app-progress": "file:plugins/course-apps/progress",
"@openedx-plugins/course-app-teams": "file:plugins/course-apps/teams",
"@openedx-plugins/course-app-wiki": "file:plugins/course-apps/wiki",
"@openedx-plugins/course-app-xpert_unit_summary": "file:plugins/course-apps/xpert_unit_summary",
"@openedx/frontend-plugin-framework": "^1.1.0",
"@openedx/paragon": "^22.2.1",
"@reduxjs/toolkit": "1.9.7",
"@tanstack/react-query": "4.36.1",
"broadcast-channel": "^7.0.0",
"classnames": "2.2.6",
"core-js": "3.8.1",
"email-validator": "2.0.4",
@@ -61,16 +75,19 @@
"formik": "2.2.6",
"jszip": "^3.10.1",
"lodash": "4.17.21",
"meilisearch": "^0.38.0",
"moment": "2.29.4",
"prop-types": "15.7.2",
"prop-types": "^15.8.1",
"react": "17.0.2",
"react-datepicker": "^4.13.0",
"react-dom": "17.0.2",
"react-error-boundary": "^4.0.13",
"react-helmet": "^6.1.0",
"react-redux": "7.2.9",
"react-responsive": "9.0.2",
"react-router": "6.16.0",
"react-router-dom": "6.16.0",
"react-select": "5.8.0",
"react-textarea-autosize": "^8.4.1",
"react-transition-group": "4.4.5",
"redux": "4.0.5",
@@ -81,16 +98,19 @@
},
"devDependencies": {
"@edx/browserslist-config": "1.2.0",
"@edx/frontend-build": "13.0.5",
"@edx/react-unit-test-utils": "^1.7.0",
"@edx/react-unit-test-utils": "^2.0.0",
"@edx/reactifex": "^1.0.3",
"@edx/stylelint-config-edx": "^2.3.0",
"@edx/stylelint-config-edx": "2.3.0",
"@edx/typescript-config": "^1.0.1",
"@openedx/frontend-build": "13.1.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.2.1",
"axios": "^0.28.0",
"axios-mock-adapter": "1.22.0",
"eslint-import-resolver-webpack": "^0.13.8",
"fetch-mock-jest": "^1.5.1",
"glob": "7.2.3",
"husky": "7.0.4",
"jest-canvas-mock": "^2.5.2",

View File

@@ -0,0 +1,31 @@
import React from 'react';
import PropTypes from 'prop-types';
import { useIntl } from '@edx/frontend-platform/i18n';
import AppSettingsModal from 'CourseAuthoring/pages-and-resources/app-settings-modal/AppSettingsModal';
import messages from './messages';
/**
* Settings widget for the "calculator" Course App.
* @param {{onClose: () => void}} props
*/
const CalculatorSettings = ({ onClose }) => {
const intl = useIntl();
return (
<AppSettingsModal
appId="calculator"
title={intl.formatMessage(messages.heading)}
enableAppHelp={intl.formatMessage(messages.enableCalculatorHelp)}
enableAppLabel={intl.formatMessage(messages.enableCalculatorLabel)}
learnMoreText={intl.formatMessage(messages.enableCalculatorLink)}
onClose={onClose}
/>
);
};
CalculatorSettings.propTypes = {
onClose: PropTypes.func.isRequired,
};
export default CalculatorSettings;

View File

@@ -0,0 +1,17 @@
{
"name": "@openedx-plugins/course-app-calculator",
"version": "0.1.0",
"description": "Calculator configuration for courses using it",
"peerDependencies": {
"@edx/frontend-app-course-authoring": "*",
"@edx/frontend-platform": "*",
"@openedx/paragon": "*",
"prop-types": "*",
"react": "*"
},
"peerDependenciesMeta": {
"@edx/frontend-app-course-authoring": {
"optional": true
}
}
}

View File

@@ -0,0 +1,31 @@
import React from 'react';
import PropTypes from 'prop-types';
import { useIntl } from '@edx/frontend-platform/i18n';
import AppSettingsModal from 'CourseAuthoring/pages-and-resources/app-settings-modal/AppSettingsModal';
import messages from './messages';
/**
* Settings widget for the "edxnotes" Course App.
* @param {{onClose: () => void}} props
*/
const NotesSettings = ({ onClose }) => {
const intl = useIntl();
return (
<AppSettingsModal
appId="edxnotes"
title={intl.formatMessage(messages.heading)}
enableAppHelp={intl.formatMessage(messages.enableNotesHelp)}
enableAppLabel={intl.formatMessage(messages.enableNotesLabel)}
learnMoreText={intl.formatMessage(messages.enableNotesLink)}
onClose={onClose}
/>
);
};
NotesSettings.propTypes = {
onClose: PropTypes.func.isRequired,
};
export default NotesSettings;

View File

@@ -0,0 +1,17 @@
{
"name": "@openedx-plugins/course-app-edxnotes",
"version": "0.1.0",
"description": "edxnotes configuration for courses using it",
"peerDependencies": {
"@edx/frontend-app-course-authoring": "*",
"@edx/frontend-platform": "*",
"@openedx/paragon": "*",
"prop-types": "*",
"react": "*"
},
"peerDependenciesMeta": {
"@edx/frontend-app-course-authoring": {
"optional": true
}
}
}

View File

@@ -1,16 +1,18 @@
import React from 'react';
import PropTypes from 'prop-types';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { Hyperlink } from '@edx/paragon';
import { useIntl } from '@edx/frontend-platform/i18n';
import { Hyperlink } from '@openedx/paragon';
import AppSettingsModal from 'CourseAuthoring/pages-and-resources/app-settings-modal/AppSettingsModal';
import { useModel } from 'CourseAuthoring/generic/model-store';
import AppSettingsModal from '../app-settings-modal/AppSettingsModal';
import messages from './messages';
import { useModel } from '../../generic/model-store';
const LearningAssistantSettings = ({ intl, onClose }) => {
const LearningAssistantSettings = ({ onClose }) => {
const appId = 'learning_assistant';
const appInfo = useModel('courseApps', appId);
const intl = useIntl();
// We need to render more than one link, so we use the bodyChildren prop.
const bodyChildren = (
@@ -55,8 +57,7 @@ const LearningAssistantSettings = ({ intl, onClose }) => {
};
LearningAssistantSettings.propTypes = {
intl: intlShape.isRequired,
onClose: PropTypes.func.isRequired,
};
export default injectIntl(LearningAssistantSettings);
export default LearningAssistantSettings;

View File

@@ -1,9 +1,9 @@
import React from 'react';
import { screen, waitFor } from '@testing-library/react';
import { RequestStatus } from 'CourseAuthoring/data/constants';
import { render } from 'CourseAuthoring/pages-and-resources/utils.test';
import LearningAssistantSettings from './Settings';
import { render } from '../utils.test';
import { RequestStatus } from '../../data/constants';
const onClose = () => { };

View File

@@ -0,0 +1,19 @@
{
"name": "@openedx-plugins/course-app-learning_assistant",
"version": "0.1.0",
"description": "Learning Assistant configuration for courses using it",
"peerDependencies": {
"@edx/frontend-app-course-authoring": "*",
"@edx/frontend-platform": "*",
"@openedx/paragon": "*",
"prop-types": "*",
"react": "*",
"yup": "*"
},
"peerDependenciesMeta": {
"@edx/frontend-app-course-authoring": {
"optional": true
}
}
}

View File

@@ -1,13 +1,14 @@
import React, { useEffect, useState } from 'react';
import { getConfig } from '@edx/frontend-platform';
import { FormattedMessage, injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { Form, Hyperlink } from '@edx/paragon';
import { Form, Hyperlink } from '@openedx/paragon';
import PropTypes from 'prop-types';
import messages from './messages';
import AppConfigFormDivider from 'CourseAuthoring/pages-and-resources/discussions/app-config-form/apps/shared/AppConfigFormDivider';
import { useModel } from 'CourseAuthoring/generic/model-store';
import { providerNames, bbbPlanTypes } from './constants';
import AppConfigFormDivider from '../discussions/app-config-form/apps/shared/AppConfigFormDivider';
import LiveCommonFields from './LiveCommonFields';
import { useModel } from '../../generic/model-store';
import messages from './messages';
const BbbSettings = ({
intl,

View File

@@ -15,8 +15,10 @@ import { AppProvider, PageWrap } from '@edx/frontend-platform/react';
import { IntlProvider } from '@edx/frontend-platform/i18n';
import userEvent from '@testing-library/user-event';
import initializeStore from '../../store';
import { executeThunk } from '../../utils';
import initializeStore from 'CourseAuthoring/store';
import { executeThunk } from 'CourseAuthoring/utils';
import PagesAndResourcesProvider from 'CourseAuthoring/pages-and-resources/PagesAndResourcesProvider';
import LiveSettings from './Settings';
import {
generateLiveConfigurationApiResponse,
@@ -24,11 +26,9 @@ import {
initialState,
configurationProviders,
} from './factories/mockApiResponses';
import { fetchLiveConfiguration, fetchLiveProviders } from './data/thunks';
import { providerConfigurationApiUrl, providersApiUrl } from './data/api';
import messages from './messages';
import PagesAndResourcesProvider from '../PagesAndResourcesProvider';
let axiosMock;
let container;

View File

@@ -1,8 +1,9 @@
import React from 'react';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import PropTypes from 'prop-types';
import FormikControl from 'CourseAuthoring/generic/FormikControl';
import messages from './messages';
import FormikControl from '../../generic/FormikControl';
const LiveCommonFields = ({
intl,

View File

@@ -1,18 +1,20 @@
import React, { useEffect } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { camelCase } from 'lodash';
import { SelectableBox, Icon } from '@edx/paragon';
import { Icon } from '@openedx/paragon';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { SelectableBox } from '@edx/frontend-lib-content-components';
import PropTypes from 'prop-types';
import * as Yup from 'yup';
import { useNavigate } from 'react-router-dom';
import AppSettingsModal from 'CourseAuthoring/pages-and-resources/app-settings-modal/AppSettingsModal';
import { useModel } from 'CourseAuthoring/generic/model-store';
import Loading from 'CourseAuthoring/generic/Loading';
import { RequestStatus } from 'CourseAuthoring/data/constants';
import { fetchLiveData, saveLiveConfiguration, saveLiveConfigurationAsDraft } from './data/thunks';
import { selectApp } from './data/slice';
import AppSettingsModal from '../app-settings-modal/AppSettingsModal';
import { useModel } from '../../generic/model-store';
import Loading from '../../generic/Loading';
import { iconsSrc, bbbPlanTypes } from './constants';
import { RequestStatus } from '../../data/constants';
import messages from './messages';
import ZoomSettings from './ZoomSettings';
import BBBSettings from './BBBSettings';

View File

@@ -18,8 +18,10 @@ import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
import { AppProvider, PageWrap } from '@edx/frontend-platform/react';
import { IntlProvider } from '@edx/frontend-platform/i18n';
import initializeStore from '../../store';
import { executeThunk } from '../../utils';
import initializeStore from 'CourseAuthoring/store';
import { executeThunk } from 'CourseAuthoring/utils';
import PagesAndResourcesProvider from 'CourseAuthoring/pages-and-resources/PagesAndResourcesProvider';
import LiveSettings from './Settings';
import {
generateLiveConfigurationApiResponse,
@@ -31,7 +33,6 @@ import {
import { fetchLiveConfiguration, fetchLiveProviders } from './data/thunks';
import { providerConfigurationApiUrl, providersApiUrl } from './data/api';
import messages from './messages';
import PagesAndResourcesProvider from '../PagesAndResourcesProvider';
let axiosMock;
let container;

View File

@@ -1,10 +1,11 @@
import React from 'react';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import PropTypes from 'prop-types';
import FormikControl from 'CourseAuthoring/generic/FormikControl';
import messages from './messages';
import { providerNames } from './constants';
import LiveCommonFields from './LiveCommonFields';
import FormikControl from '../../generic/FormikControl';
const ZoomSettings = ({
intl,

View File

@@ -13,8 +13,9 @@ import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
import { AppProvider, PageWrap } from '@edx/frontend-platform/react';
import { IntlProvider } from '@edx/frontend-platform/i18n';
import initializeStore from '../../store';
import { executeThunk } from '../../utils';
import initializeStore from 'CourseAuthoring/store';
import { executeThunk } from 'CourseAuthoring/utils';
import PagesAndResourcesProvider from 'CourseAuthoring/pages-and-resources/PagesAndResourcesProvider';
import LiveSettings from './Settings';
import {
generateLiveConfigurationApiResponse,
@@ -26,7 +27,6 @@ import {
import { fetchLiveConfiguration, fetchLiveProviders } from './data/thunks';
import { providerConfigurationApiUrl, providersApiUrl } from './data/api';
import messages from './messages';
import PagesAndResourcesProvider from '../PagesAndResourcesProvider';
let axiosMock;
let container;

View File

@@ -1,6 +1,6 @@
import {
GoogleMeet, MicrosoftTeams, Zoom, Bbb,
} from '@edx/paragon/icons';
} from '@openedx/paragon/icons';
export const iconsSrc = {
googleMeet: GoogleMeet,

View File

@@ -1,6 +1,6 @@
/* eslint-disable no-param-reassign */
import { createSlice } from '@reduxjs/toolkit';
import { RequestStatus } from '../../../data/constants';
import { RequestStatus } from 'CourseAuthoring/data/constants';
const slice = createSlice({
name: 'live',

View File

@@ -1,4 +1,6 @@
import { addModel, addModels, updateModel } from '../../../generic/model-store';
import { addModel, addModels, updateModel } from 'CourseAuthoring/generic/model-store';
import { RequestStatus } from 'CourseAuthoring/data/constants';
import {
getLiveConfiguration,
getLiveProviders,
@@ -7,7 +9,6 @@ import {
deNormalizeSettings,
} from './api';
import { loadApps, updateStatus, updateSaveStatus } from './slice';
import { RequestStatus } from '../../../data/constants';
function updateLiveSettingsState({
appConfig,

View File

@@ -0,0 +1,23 @@
{
"name": "@openedx-plugins/course-app-live",
"version": "0.1.0",
"description": "Live course configuration for courses using it",
"peerDependencies": {
"@edx/frontend-app-course-authoring": "*",
"@edx/frontend-lib-content-components": "*",
"@edx/frontend-platform": "*",
"@openedx/paragon": "*",
"@reduxjs/toolkit": "*",
"lodash": "*",
"prop-types": "*",
"react": "*",
"react-redux": "*",
"react-router-dom": "*",
"yup": "*"
},
"peerDependenciesMeta": {
"@edx/frontend-app-course-authoring": {
"optional": true
}
}
}

View File

@@ -4,12 +4,12 @@ import * as Yup from 'yup';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { Hyperlink } from '@edx/paragon';
import { useModel } from '../../generic/model-store';
import { Hyperlink } from '@openedx/paragon';
import { useModel } from 'CourseAuthoring/generic/model-store';
import FormSwitchGroup from '../../generic/FormSwitchGroup';
import { useAppSetting } from '../../utils';
import AppSettingsModal from '../app-settings-modal/AppSettingsModal';
import FormSwitchGroup from 'CourseAuthoring/generic/FormSwitchGroup';
import { useAppSetting } from 'CourseAuthoring/utils';
import AppSettingsModal from 'CourseAuthoring/pages-and-resources/app-settings-modal/AppSettingsModal';
import messages from './messages';
const ORASettings = ({ intl, onClose }) => {

View File

@@ -9,14 +9,14 @@ jest.mock('@edx/frontend-platform/i18n', () => ({
jest.mock('yup', () => ({
boolean: jest.fn().mockReturnValue('Yub.boolean'),
}));
jest.mock('../../generic/model-store', () => ({
jest.mock('CourseAuthoring/generic/model-store', () => ({
useModel: jest.fn().mockReturnValue({ documentationLinks: { learnMoreConfiguration: 'https://learnmore.test' } }),
}));
jest.mock('../../generic/FormSwitchGroup', () => 'FormSwitchGroup');
jest.mock('../../utils', () => ({
jest.mock('CourseAuthoring/generic/FormSwitchGroup', () => 'FormSwitchGroup');
jest.mock('CourseAuthoring/utils', () => ({
useAppSetting: jest.fn().mockReturnValue(['abitrary value', jest.fn().mockName('saveSetting')]),
}));
jest.mock('../app-settings-modal/AppSettingsModal', () => 'AppSettingsModal');
jest.mock('CourseAuthoring/pages-and-resources/app-settings-modal/AppSettingsModal', () => 'AppSettingsModal');
const props = {
onClose: jest.fn().mockName('onClose'),

View File

@@ -0,0 +1,19 @@
{
"name": "@openedx-plugins/course-app-ora_settings",
"version": "0.1.0",
"description": "Open Response Assessment configuration for courses using it",
"peerDependencies": {
"@edx/frontend-app-course-authoring": "*",
"@edx/frontend-platform": "*",
"@openedx/paragon": "*",
"prop-types": "*",
"react": "*",
"yup": "*"
},
"peerDependenciesMeta": {
"@edx/frontend-app-course-authoring": {
"optional": true
}
}
}

View File

@@ -11,17 +11,18 @@ import { getAuthenticatedUser } from '@edx/frontend-platform/auth';
import { injectIntl, intlShape, FormattedMessage } from '@edx/frontend-platform/i18n';
import {
ActionRow, Alert, Badge, Form, Hyperlink, ModalDialog, StatefulButton,
} from '@edx/paragon';
} from '@openedx/paragon';
import ExamsApiService from 'CourseAuthoring/data/services/ExamsApiService';
import StudioApiService from 'CourseAuthoring/data/services/StudioApiService';
import Loading from 'CourseAuthoring/generic/Loading';
import ConnectionErrorAlert from 'CourseAuthoring/generic/ConnectionErrorAlert';
import FormSwitchGroup from 'CourseAuthoring/generic/FormSwitchGroup';
import { useModel } from 'CourseAuthoring/generic/model-store';
import PermissionDeniedAlert from 'CourseAuthoring/generic/PermissionDeniedAlert';
import { useIsMobile } from 'CourseAuthoring/utils';
import { PagesAndResourcesContext } from 'CourseAuthoring/pages-and-resources/PagesAndResourcesProvider';
import ExamsApiService from '../../data/services/ExamsApiService';
import StudioApiService from '../../data/services/StudioApiService';
import Loading from '../../generic/Loading';
import ConnectionErrorAlert from '../../generic/ConnectionErrorAlert';
import FormSwitchGroup from '../../generic/FormSwitchGroup';
import { useModel } from '../../generic/model-store';
import PermissionDeniedAlert from '../../generic/PermissionDeniedAlert';
import { useIsMobile } from '../../utils';
import { PagesAndResourcesContext } from '../PagesAndResourcesProvider';
import messages from './messages';
const ProctoringSettings = ({ intl, onClose }) => {

View File

@@ -9,10 +9,10 @@ import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
import { IntlProvider, injectIntl } from '@edx/frontend-platform/i18n';
import { AppProvider } from '@edx/frontend-platform/react';
import StudioApiService from '../../data/services/StudioApiService';
import ExamsApiService from '../../data/services/ExamsApiService';
import initializeStore from '../../store';
import PagesAndResourcesProvider from '../PagesAndResourcesProvider';
import StudioApiService from 'CourseAuthoring/data/services/StudioApiService';
import ExamsApiService from 'CourseAuthoring/data/services/ExamsApiService';
import initializeStore from 'CourseAuthoring/store';
import PagesAndResourcesProvider from 'CourseAuthoring/pages-and-resources/PagesAndResourcesProvider';
import ProctoredExamSettings from './Settings';
const defaultProps = {

View File

@@ -0,0 +1,20 @@
{
"name": "@openedx-plugins/course-app-proctoring",
"version": "0.1.0",
"description": "Proctoring configuration for courses using it",
"peerDependencies": {
"@edx/frontend-app-course-authoring": "*",
"@edx/frontend-platform": "*",
"@openedx/paragon": "*",
"classnames": "*",
"email-validator": "*",
"react": "*",
"prop-types": "*",
"moment": "*"
},
"peerDependenciesMeta": {
"@edx/frontend-app-course-authoring": {
"optional": true
}
}
}

View File

@@ -3,9 +3,9 @@ import PropTypes from 'prop-types';
import React from 'react';
import * as Yup from 'yup';
import { getConfig } from '@edx/frontend-platform';
import FormSwitchGroup from '../../generic/FormSwitchGroup';
import { useAppSetting } from '../../utils';
import AppSettingsModal from '../app-settings-modal/AppSettingsModal';
import FormSwitchGroup from 'CourseAuthoring/generic/FormSwitchGroup';
import { useAppSetting } from 'CourseAuthoring/utils';
import AppSettingsModal from 'CourseAuthoring/pages-and-resources/app-settings-modal/AppSettingsModal';
import messages from './messages';
const ProgressSettings = ({ intl, onClose }) => {

View File

@@ -0,0 +1,18 @@
{
"name": "@openedx-plugins/course-app-progress",
"version": "0.1.0",
"description": "Progress configuration for courses using it",
"peerDependencies": {
"@edx/frontend-app-course-authoring": "*",
"@edx/frontend-platform": "*",
"@openedx/paragon": "*",
"prop-types": "*",
"react": "*",
"yup": "*"
},
"peerDependenciesMeta": {
"@edx/frontend-app-course-authoring": {
"optional": true
}
}
}

View File

@@ -1,12 +1,13 @@
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { Button, Form, TransitionReplace } from '@edx/paragon';
import { Button, Form, TransitionReplace } from '@openedx/paragon';
import PropTypes from 'prop-types';
import React, { useState } from 'react';
import { GroupTypes, TeamSizes } from '../../data/constants';
import { GroupTypes, TeamSizes } from 'CourseAuthoring/data/constants';
import CollapsableEditor from '../../generic/CollapsableEditor';
import FormikControl from '../../generic/FormikControl';
import CollapsableEditor from 'CourseAuthoring/generic/CollapsableEditor';
import FormikControl from 'CourseAuthoring/generic/FormikControl';
import messages from './messages';
import { isGroupTypeEnabled } from './utils';
// Maps a team type to its corresponding intl message
const TeamTypeNameMessage = {
@@ -14,6 +15,10 @@ const TeamTypeNameMessage = {
label: messages.groupTypeOpen,
description: messages.groupTypeOpenDescription,
},
[GroupTypes.OPEN_MANAGED]: {
label: messages.groupTypeOpenManaged,
description: messages.groupTypeOpenManagedDescription,
},
[GroupTypes.PUBLIC_MANAGED]: {
label: messages.groupTypePublicManaged,
description: messages.groupTypePublicManagedDescription,
@@ -105,7 +110,7 @@ const GroupEditor = ({
onChange={onChange}
onBlur={onBlur}
>
{Object.values(GroupTypes).map(groupType => (
{Object.values(GroupTypes).map(groupType => isGroupTypeEnabled(groupType) && (
<Form.Radio
key={groupType}
value={groupType}

View File

@@ -0,0 +1,102 @@
import React from 'react';
import { render, fireEvent } from '@testing-library/react';
import { useFormikContext } from 'formik';
import { IntlProvider } from '@edx/frontend-platform/i18n';
import GroupEditor from './GroupEditor';
import messages from './messages';
jest.mock('formik', () => ({
...jest.requireActual('formik'),
useFormikContext: jest.fn(),
}));
describe('GroupEditor', () => {
const mockIntl = { formatMessage: jest.fn() };
const mockGroup = {
id: '1',
name: 'Test Group',
description: 'Test Group Description',
type: 'open',
maxTeamSize: 5,
};
const mockProps = {
intl: mockIntl,
fieldNameCommonBase: 'test',
group: mockGroup,
onDelete: jest.fn(),
onChange: jest.fn(),
onBlur: jest.fn(),
errors: {},
};
const renderComponent = (overrideProps = {}) => render(
<IntlProvider locale="en" messages={{}}>
<GroupEditor {...mockProps} {...overrideProps} />
</IntlProvider>,
);
beforeEach(() => {
useFormikContext.mockReturnValue({
touched: {},
errors: {},
handleChange: jest.fn(),
handleBlur: jest.fn(),
setFieldError: jest.fn(),
});
jest.clearAllMocks();
});
test('renders without errors', () => {
renderComponent();
});
test('renders the group name and description', () => {
const { getByText } = renderComponent();
expect(getByText('Test Group')).toBeInTheDocument();
expect(getByText('Test Group Description')).toBeInTheDocument();
});
describe('group types messages', () => {
test('group type open message', () => {
const { getByLabelText, getByText } = renderComponent();
const expandButton = getByLabelText('Expand group editor');
expect(expandButton).toBeInTheDocument();
fireEvent.click(expandButton);
expect(getByText(messages.groupTypeOpenDescription.defaultMessage)).toBeInTheDocument();
});
test('group type public_managed message', () => {
const publicManagedGroupMock = {
id: '2',
name: 'Test Group',
description: 'Test Group Description',
type: 'public_managed',
maxTeamSize: 5,
};
const { getByLabelText, getByText } = renderComponent({ group: publicManagedGroupMock });
const expandButton = getByLabelText('Expand group editor');
expect(expandButton).toBeInTheDocument();
fireEvent.click(expandButton);
expect(getByText(messages.groupTypePublicManagedDescription.defaultMessage)).toBeInTheDocument();
});
test('group type private_managed message', () => {
const privateManagedGroupMock = {
id: '3',
name: 'Test Group',
description: 'Test Group Description',
type: 'private_managed',
maxTeamSize: 5,
};
const { getByLabelText, getByText } = renderComponent({ group: privateManagedGroupMock });
const expandButton = getByLabelText('Expand group editor');
expect(expandButton).toBeInTheDocument();
fireEvent.click(expandButton);
expect(getByText(messages.groupTypePrivateManagedDescription.defaultMessage)).toBeInTheDocument();
});
});
});

View File

@@ -1,16 +1,16 @@
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { Button, Form } from '@edx/paragon';
import { Add } from '@edx/paragon/icons';
import { Button, Form } from '@openedx/paragon';
import { Add } from '@openedx/paragon/icons';
import { FieldArray } from 'formik';
import PropTypes from 'prop-types';
import React from 'react';
import { v4 as uuid } from 'uuid';
import * as Yup from 'yup';
import { GroupTypes, TeamSizes } from '../../data/constants';
import FormikControl from '../../generic/FormikControl';
import { setupYupExtensions, useAppSetting } from '../../utils';
import AppSettingsModal from '../app-settings-modal/AppSettingsModal';
import { GroupTypes, TeamSizes } from 'CourseAuthoring/data/constants';
import FormikControl from 'CourseAuthoring/generic/FormikControl';
import { setupYupExtensions, useAppSetting } from 'CourseAuthoring/utils';
import AppSettingsModal from 'CourseAuthoring/pages-and-resources/app-settings-modal/AppSettingsModal';
import GroupEditor from './GroupEditor';
import messages from './messages';

View File

@@ -93,6 +93,14 @@ const messages = defineMessages({
id: 'authoring.pagesAndResources.teams.group.types.open',
defaultMessage: 'Open',
},
groupTypeOpenManaged: {
id: 'authoring.pagesAndResources.teams.group.types.open_managed',
defaultMessage: 'Open managed',
},
groupTypeOpenManagedDescription: {
id: 'authoring.pagesAndResources.teams.group.types.open_managed.description',
defaultMessage: 'Only course staff can create teams. Learners can see, join and leave teams.',
},
groupTypeOpenDescription: {
id: 'authoring.pagesAndResources.teams.group.types.open.description',
defaultMessage: 'Learners can create, join, leave, and see other teams',

View File

@@ -0,0 +1,20 @@
{
"name": "@openedx-plugins/course-app-teams",
"version": "0.1.0",
"description": "Teams configuration for courses using it",
"peerDependencies": {
"@edx/frontend-app-course-authoring": "*",
"@edx/frontend-platform": "*",
"@openedx/paragon": "*",
"formik": "*",
"prop-types": "*",
"react": "*",
"uuid": "*",
"yup": "*"
},
"peerDependenciesMeta": {
"@edx/frontend-app-course-authoring": {
"optional": true
}
}
}

View File

@@ -0,0 +1,23 @@
/* eslint-disable import/prefer-default-export */
import { getConfig } from '@edx/frontend-platform';
import { GroupTypes } from 'CourseAuthoring/data/constants';
/**
* Check if a group type is enabled by the current configuration.
* This is a temporary workaround to disable the OPEN MANAGED team type until it is fully adopted.
* For more information, see: https://openedx.atlassian.net/wiki/spaces/COMM/pages/3885760525/Open+Managed+Group+Type
* @param {string} groupType - the group type to check
* @returns {boolean} - true if the group type is enabled
*/
export const isGroupTypeEnabled = (groupType) => {
const enabledTypesByDefault = [
GroupTypes.OPEN,
GroupTypes.PUBLIC_MANAGED,
GroupTypes.PRIVATE_MANAGED,
];
const enabledTypesByConfig = {
[GroupTypes.OPEN_MANAGED]: getConfig().ENABLE_OPEN_MANAGED_TEAM_TYPE,
};
return enabledTypesByDefault.includes(groupType) || enabledTypesByConfig[groupType] || false;
};

View File

@@ -0,0 +1,39 @@
import { getConfig } from '@edx/frontend-platform';
import { GroupTypes } from 'CourseAuthoring/data/constants';
import { isGroupTypeEnabled } from './utils';
jest.mock('@edx/frontend-platform', () => ({ getConfig: jest.fn() }));
describe('teams utils', () => {
describe('isGroupTypeEnabled', () => {
beforeEach(() => {
jest.clearAllMocks();
});
test('returns true if the group type is enabled', () => {
getConfig.mockReturnValue({ ENABLE_OPEN_MANAGED_TEAM_TYPE: false });
expect(isGroupTypeEnabled(GroupTypes.OPEN)).toBe(true);
expect(isGroupTypeEnabled(GroupTypes.PUBLIC_MANAGED)).toBe(true);
expect(isGroupTypeEnabled(GroupTypes.PRIVATE_MANAGED)).toBe(true);
});
test('returns false if the OPEN_MANAGED group is not enabled', () => {
getConfig.mockReturnValue({ ENABLE_OPEN_MANAGED_TEAM_TYPE: false });
expect(isGroupTypeEnabled(GroupTypes.OPEN_MANAGED)).toBe(false);
});
test('returns true if the OPEN_MANAGED group is enabled', () => {
getConfig.mockReturnValue({ ENABLE_OPEN_MANAGED_TEAM_TYPE: true });
expect(isGroupTypeEnabled(GroupTypes.OPEN_MANAGED)).toBe(true);
});
test('returns false if the group is invalid', () => {
getConfig.mockReturnValue({ ENABLE_OPEN_MANAGED_TEAM_TYPE: true });
expect(isGroupTypeEnabled('FOO')).toBe(false);
});
test('returns false if the group is null', () => {
getConfig.mockReturnValue({ ENABLE_OPEN_MANAGED_TEAM_TYPE: true });
expect(isGroupTypeEnabled(null)).toBe(false);
});
});
});

View File

@@ -3,9 +3,9 @@ import PropTypes from 'prop-types';
import React from 'react';
import * as Yup from 'yup';
import FormSwitchGroup from '../../generic/FormSwitchGroup';
import { useAppSetting } from '../../utils';
import AppSettingsModal from '../app-settings-modal/AppSettingsModal';
import FormSwitchGroup from 'CourseAuthoring/generic/FormSwitchGroup';
import { useAppSetting } from 'CourseAuthoring/utils';
import AppSettingsModal from 'CourseAuthoring/pages-and-resources/app-settings-modal/AppSettingsModal';
import messages from './messages';
const WikiSettings = ({ intl, onClose }) => {

View File

@@ -0,0 +1,18 @@
{
"name": "@openedx-plugins/course-app-wiki",
"version": "0.1.0",
"description": "Wiki configuration for courses using it",
"peerDependencies": {
"@edx/frontend-app-course-authoring": "*",
"@edx/frontend-platform": "*",
"@openedx/paragon": "*",
"prop-types": "*",
"react": "*",
"yup": "*"
},
"peerDependenciesMeta": {
"@edx/frontend-app-course-authoring": {
"optional": true
}
}
}

View File

@@ -0,0 +1,4 @@
Xpert Unit Summaries Configuration Plugin
=========================================
Install this using ``npm install plugins/course-apps/xpert_unit_summary/ --no-save``.

View File

@@ -2,8 +2,8 @@ import React, { useCallback, useContext, useEffect } from 'react';
import { useDispatch } from 'react-redux';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { PagesAndResourcesContext } from 'CourseAuthoring/pages-and-resources/PagesAndResourcesProvider';
import { useNavigate } from 'react-router-dom';
import { PagesAndResourcesContext } from '../PagesAndResourcesProvider';
import SettingsModal from './settings-modal/SettingsModal';
import messages from './messages';

View File

@@ -10,12 +10,13 @@ import {
queryByTestId, render, waitFor, getByText, fireEvent,
} from '@testing-library/react';
import MockAdapter from 'axios-mock-adapter';
import PagesAndResourcesProvider from '../PagesAndResourcesProvider';
import { XpertUnitSummarySettings } from './index';
import initializeStore from '../../store';
import PagesAndResourcesProvider from 'CourseAuthoring/pages-and-resources/PagesAndResourcesProvider';
import initializeStore from 'CourseAuthoring/store';
import { executeThunk } from 'CourseAuthoring/utils';
import XpertUnitSummarySettings from './Settings';
import * as API from './data/api';
import * as Thunks from './data/thunks';
import { executeThunk } from '../../utils';
const courseId = 'course-v1:edX+TestX+Test_Course';
let axiosMock;

View File

@@ -1,12 +1,11 @@
import { updateSavingStatus, updateLoadingStatus, updateResetStatus } from 'CourseAuthoring/pages-and-resources/data/slice';
import { RequestStatus } from 'CourseAuthoring/data/constants';
import { addModel, updateModel } from 'CourseAuthoring/generic/model-store';
import {
getXpertSettings, postXpertSettings, getXpertPluginConfigurable, deleteXpertSettings,
} from './api';
import { updateSavingStatus, updateLoadingStatus, updateResetStatus } from '../../data/slice';
import { RequestStatus } from '../../../data/constants';
import { addModel, updateModel } from '../../../generic/model-store';
export function updateXpertSettings(courseId, state) {
return async (dispatch) => {
dispatch(updateSavingStatus({ status: RequestStatus.IN_PROGRESS }));

View File

@@ -0,0 +1,21 @@
{
"name": "@openedx-plugins/course-app-xpert_unit_summary",
"version": "0.1.0",
"description": "Xpert Unit Summaries configuration for courses using it",
"peerDependencies": {
"@edx/frontend-app-course-authoring": "*",
"@edx/frontend-platform": "*",
"@openedx/paragon": "*",
"formik": "*",
"prop-types": "*",
"yup": "*",
"react": "*",
"react-redux": "*",
"react-router-dom": "*"
},
"peerDependenciesMeta": {
"@edx/frontend-app-course-authoring": {
"optional": true
}
}
}

View File

@@ -11,10 +11,10 @@ import {
Tooltip,
TransitionReplace,
Hyperlink,
} from '@edx/paragon';
} from '@openedx/paragon';
import {
Info, CheckCircleOutline, SpinnerSimple,
} from '@edx/paragon/icons';
} from '@openedx/paragon/icons';
import { Formik } from 'formik';
import PropTypes from 'prop-types';
@@ -24,22 +24,25 @@ import React, {
import { useDispatch, useSelector } from 'react-redux';
import * as Yup from 'yup';
import { RequestStatus } from '../../../data/constants';
import ConnectionErrorAlert from '../../../generic/ConnectionErrorAlert';
import FormSwitchGroup from '../../../generic/FormSwitchGroup';
import Loading from '../../../generic/Loading';
import { useModel } from '../../../generic/model-store';
import PermissionDeniedAlert from '../../../generic/PermissionDeniedAlert';
import { useIsMobile } from '../../../utils';
import { getLoadingStatus, getSavingStatus, getResetStatus } from '../../data/selectors';
import { updateSavingStatus, updateResetStatus } from '../../data/slice';
import { RequestStatus } from 'CourseAuthoring/data/constants';
import ConnectionErrorAlert from 'CourseAuthoring/generic/ConnectionErrorAlert';
import FormSwitchGroup from 'CourseAuthoring/generic/FormSwitchGroup';
import Loading from 'CourseAuthoring/generic/Loading';
import { useModel } from 'CourseAuthoring/generic/model-store';
import PermissionDeniedAlert from 'CourseAuthoring/generic/PermissionDeniedAlert';
import { useIsMobile } from 'CourseAuthoring/utils';
import { getLoadingStatus, getSavingStatus, getResetStatus } from 'CourseAuthoring/pages-and-resources/data/selectors';
import { updateSavingStatus, updateResetStatus } from 'CourseAuthoring/pages-and-resources/data/slice';
import AppConfigFormDivider from 'CourseAuthoring/pages-and-resources/discussions/app-config-form/apps/shared/AppConfigFormDivider';
import { PagesAndResourcesContext } from 'CourseAuthoring/pages-and-resources/PagesAndResourcesProvider';
import { updateXpertSettings, resetXpertSettings, removeXpertSettings } from '../data/thunks';
import AppConfigFormDivider from '../../discussions/app-config-form/apps/shared/AppConfigFormDivider';
import { PagesAndResourcesContext } from '../../PagesAndResourcesProvider';
import messages from './messages';
import appInfo from '../appInfo';
import ResetIcon from './ResetIcon';
import './SettingsModal.scss';
const AppSettingsForm = ({
formikProps, children, showForm,
}) => children && (

View File

@@ -1,3 +1,6 @@
@import "~@edx/brand/paragon/variables";
@import "~@openedx/paragon/scss/core/utilities-only";
.summary-radio {
display: flex;
align-items: center;

View File

@@ -4,6 +4,7 @@ import {
} from 'react-router-dom';
import { getConfig } from '@edx/frontend-platform';
import { PageWrap } from '@edx/frontend-platform/react';
import { Textbooks } from 'CourseAuthoring/textbooks';
import CourseAuthoringPage from './CourseAuthoringPage';
import { PagesAndResources } from './pages-and-resources';
import EditorContainer from './editors/EditorContainer';
@@ -17,9 +18,12 @@ import { GradingSettings } from './grading-settings';
import CourseTeam from './course-team/CourseTeam';
import { CourseUpdates } from './course-updates';
import { CourseUnit } from './course-unit';
import { Certificates } from './certificates';
import CourseExportPage from './export-page/CourseExportPage';
import CourseImportPage from './import-page/CourseImportPage';
import { DECODED_ROUTES } from './constants';
import CourseChecklist from './course-checklist';
import GroupConfigurations from './group-configurations';
/**
* As of this writing, these routes are mounted at a path prefixed with the following:
@@ -73,17 +77,18 @@ const CourseAuthoringRoutes = () => {
/>
{DECODED_ROUTES.COURSE_UNIT.map((path) => (
<Route
key={path}
path={path}
element={<PageWrap><CourseUnit courseId={courseId} /></PageWrap>}
/>
))}
<Route
path="editor/course-videos/:blockId"
element={getConfig().ENABLE_NEW_EDITOR_PAGES === 'true' ? <PageWrap><VideoSelectorContainer courseId={courseId} /></PageWrap> : null}
element={<PageWrap><VideoSelectorContainer courseId={courseId} /></PageWrap>}
/>
<Route
path="editor/:blockType/:blockId?"
element={getConfig().ENABLE_NEW_EDITOR_PAGES === 'true' ? <PageWrap><EditorContainer courseId={courseId} /></PageWrap> : null}
element={<PageWrap><EditorContainer courseId={courseId} /></PageWrap>}
/>
<Route
path="settings/details"
@@ -97,6 +102,10 @@ const CourseAuthoringRoutes = () => {
path="course_team"
element={<PageWrap><CourseTeam courseId={courseId} /></PageWrap>}
/>
<Route
path="group_configurations"
element={<PageWrap><GroupConfigurations courseId={courseId} /></PageWrap>}
/>
<Route
path="settings/advanced"
element={<PageWrap><AdvancedSettings courseId={courseId} /></PageWrap>}
@@ -109,6 +118,18 @@ const CourseAuthoringRoutes = () => {
path="export"
element={<PageWrap><CourseExportPage courseId={courseId} /></PageWrap>}
/>
<Route
path="checklists"
element={<PageWrap><CourseChecklist courseId={courseId} /></PageWrap>}
/>
<Route
path="certificates"
element={<PageWrap><Certificates courseId={courseId} /></PageWrap>}
/>
<Route
path="textbooks"
element={<PageWrap><Textbooks courseId={courseId} /></PageWrap>}
/>
</Routes>
</CourseAuthoringPage>
);

View File

@@ -0,0 +1,16 @@
export default {
content: {
id: 67,
userId: 3,
created: '2024-01-16T13:09:11.540615Z',
purpose: 'clipboard',
status: 'ready',
blockType: 'vertical',
blockTypeDisplay: 'Unit',
olxUrl: 'http://localhost:18010/api/content-staging/v1/staged-content/67/olx',
displayName: 'Introduction: Video and Sequences',
},
sourceUsageKey: 'block-v1:edX+DemoX+Demo_Course+type@vertical+block@vertical_0270f6de40fc',
sourceContextTitle: 'Demonstration Course',
sourceEditUrl: 'http://localhost:18010/container/block-v1:edX+DemoX+Demo_Course+type@vertical+block@vertical_0270f6de40fc',
};

View File

@@ -0,0 +1,16 @@
export default {
content: {
id: 69,
userId: 3,
created: '2024-01-16T13:33:21.314439Z',
purpose: 'clipboard',
status: 'ready',
blockType: 'html',
blockTypeDisplay: 'Text',
olxUrl: 'http://localhost:18010/api/content-staging/v1/staged-content/69/olx',
displayName: 'Blank HTML Page',
},
sourceUsageKey: 'block-v1:edX+DemoX+Demo_Course+type@html+block@html1',
sourceContextTitle: 'Demonstration Course',
sourceEditUrl: 'http://localhost:18010/container/block-v1:edX+DemoX+Demo_Course+type@vertical+block@vertical1',
};

2
src/__mocks__/index.js Normal file
View File

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

View File

@@ -0,0 +1,98 @@
import React from 'react';
import PropTypes from 'prop-types';
import { injectIntl, FormattedMessage } from '@edx/frontend-platform/i18n';
import { Hyperlink, MailtoLink, Stack } from '@openedx/paragon';
import messages from './messages';
const AccessibilityBody = ({
communityAccessibilityLink,
email,
}) => (
<div className="mt-5">
<header>
<h2 className="mb-4 pb-1">
<FormattedMessage {...messages.a11yBodyPageHeader} />
</h2>
</header>
<Stack gap={2.5}>
<div className="small">
<FormattedMessage
{...messages.a11yBodyIntroGraph}
values={{
communityAccessibilityLink: (
<Hyperlink
destination={communityAccessibilityLink}
data-testid="accessibility-page-link"
>
Website Accessibility Policy
</Hyperlink>
),
}}
/>
</div>
<div className="small">
<FormattedMessage {...messages.a11yBodyStepsHeader} />
</div>
<ol className="small m-0">
<li>
<FormattedMessage
{...messages.a11yBodyEmailHeading}
values={{
emailElement: (
<MailtoLink
to={email}
data-testid="email-element"
>
{email}
</MailtoLink>
),
}}
/>
<ul>
<li>
<FormattedMessage {...messages.a11yBodyNameEmail} />
</li>
<li>
<FormattedMessage {...messages.a11yBodyInstitution} />
</li>
<li>
<FormattedMessage {...messages.a11yBodyBarrier} />
</li>
<li>
<FormattedMessage {...messages.a11yBodyTimeConstraints} />
</li>
</ul>
</li>
<li>
<FormattedMessage {...messages.a11yBodyReceipt} />
</li>
<li>
<FormattedMessage {...messages.a11yBodyExtraInfo} />
</li>
</ol>
<div className="small">
<FormattedMessage
{...messages.a11yBodyA11yFeedback}
values={{
emailElement: (
<MailtoLink
to={email}
data-testid="email-element"
>
{email}
</MailtoLink>
),
}}
/>
</div>
</Stack>
</div>
);
AccessibilityBody.propTypes = {
communityAccessibilityLink: PropTypes.string.isRequired,
email: PropTypes.string.isRequired,
};
export default injectIntl(AccessibilityBody);

View File

@@ -0,0 +1,46 @@
import {
render,
screen,
} from '@testing-library/react';
import { AppProvider } from '@edx/frontend-platform/react';
import { IntlProvider } from '@edx/frontend-platform/i18n';
import { initializeMockApp } from '@edx/frontend-platform';
import initializeStore from '../../store';
import AccessibilityBody from './index';
let store;
const renderComponent = () => {
render(
<IntlProvider locale="en">
<AppProvider store={store}>
<AccessibilityBody
communityAccessibilityLink="http://example.com"
email="example@example.com"
/>
</AppProvider>
</IntlProvider>,
);
};
describe('<AccessibilityBody />', () => {
describe('renders', () => {
beforeEach(async () => {
initializeMockApp({
authenticatedUser: {
userId: 3,
username: 'abc123',
administrator: false,
roles: [],
},
});
store = initializeStore({});
});
it('contains links', () => {
renderComponent();
expect(screen.getAllByTestId('email-element')).toHaveLength(2);
expect(screen.getAllByTestId('accessibility-page-link')).toHaveLength(1);
});
});
});

View File

@@ -0,0 +1,3 @@
import AccessibilityBody from './AccessibilityBody';
export default AccessibilityBody;

View File

@@ -0,0 +1,111 @@
import { defineMessages } from '@edx/frontend-platform/i18n';
const messages = defineMessages({
a11yBodyPolicyLink: {
id: 'a11yBodyPolicyLink',
defaultMessage: 'Website Accessibility Policy',
description: 'Title for link to full accessibility policy.',
},
a11yBodyPageHeader: {
id: 'a11yBodyPageHeader',
defaultMessage: 'Individualized Accessibility Process for Course Creators',
description: 'Heading for studio\'s accessibility policy page.',
},
a11yBodyIntroGraph: {
id: 'a11yBodyIntroGraph',
defaultMessage: `At edX, we seek to understand and respect the unique needs and perspectives of the edX global community.
We value every course team and are committed to expanding access to all, including course team creators and authors with
disabilities. To that end, we have adopted a {communityAccessibilityLink} and this process to allow course team creators
and authors to request assistance if they are unable to develop and post content on our platform via Studio because of their
disabilities.`,
description: 'Introductory paragraph outlining why we care about accessibility, and what we\'re doing about it.',
},
a11yBodyStepsHeader: {
id: 'a11yBodyStepsHeader',
defaultMessage: 'Course team creators and authors needing such assistance should take the following steps:',
description: 'Heading for list of steps authors can take for accessibility requests.',
},
a11yBodyEdxResponse: {
id: 'a11yBodyEdxResponse',
defaultMessage: `'We will communicate with you about your preferences and needs in determining the appropriate solution, although
the ultimate decision will be ours, provided that the solution is effective and timely. The factors we will consider in choosing
an accessibility solution are: effectiveness; timeliness (relative to your deadlines); ease of implementation; and ease of use for
you. We will notify you of the decision and explain the basis for our decision within 10 business days of discussing with you.`,
description: 'Paragraph outlining how we will select an accessibility solution.',
},
a11yBodyEdxFollowUp: {
id: 'a11yBodyEdxFollowUp',
defaultMessage: `Thereafter, we will communicate with you on a weekly basis regarding our evaluation, decision, and progress in
implementing the accessibility solution. We will notify you when implementation of your accessibility solution is complete and
will follow-up with you as may be necessary to see if the solution was effective.`,
description: 'Paragraph outlining how we will follow-up with you during and after implementing an accessibility solution.',
},
a11yBodyOngoingSupport: {
id: 'a11yBodyOngoingSupport',
defaultMessage: 'EdX will provide ongoing technical support as needed and will address any additional issues that arise after the initial course creation.',
description: 'A statement of ongoing support.',
},
a11yBodyA11yFeedback: {
id: 'a11yBodyA11yFeedback',
defaultMessage: 'Please direct any questions or suggestions on how to improve the accessibility of Studio to {emailElement} or use the form below. We welcome your feedback.',
description: 'Contact information heading for those with accessibility issues or suggestions.',
},
a11yBodyEmailHeading: {
id: 'a11yBodyEmailHeading',
defaultMessage: 'Send an email to {emailElement} with the following information:',
description: 'Heading for list of information required when you email us.',
},
a11yBodyNameEmail: {
id: 'a11yBodyNameEmail',
defaultMessage: 'your name and email address;',
description: 'Your contact information.',
},
a11yBodyInstitution: {
id: 'a11yBodyInstitution',
defaultMessage: 'the edX member institution that you are affiliated with;',
description: 'edX affiliate information.',
},
a11yBodyBarrier: {
id: 'a11yBodyBarrier',
defaultMessage: 'a brief description of the challenge or barrier to access that you are experiencing; and',
description: 'Accessibility problem information.',
},
a11yBodyTimeConstraints: {
id: 'a11yBodyTimeConstraints',
defaultMessage: 'how soon you need access and for how long (e.g., a planned course start date or in connection with a course-related deadline such as a final essay).',
description: 'Time contstraint information.',
},
a11yBodyReceipt: {
id: 'a11yBodyReceipt',
defaultMessage: 'The edX Support Team will respond to confirm receipt and forward your request to the edX Partner Manager for your institution and the edX Website Accessibility Specialist.',
description: 'Paragraph outlining what steps edX will take immediately.',
},
a11yBodyExtraInfo: {
id: 'a11yBodyExtraInfo',
defaultMessage: `With guidance from the Website Accessibility Specialist, edX will contact you to discuss your request and gather
additional information from you about your preferences and needs, to determine if there's a workable solution that edX is able to support.`,
description: 'Paragraph outlining how and when edX will reach out to you.',
},
a11yBodyFixesListHeader: {
id: 'a11yBodyFixesListHeader',
defaultMessage: 'EdX will assist you promptly and thoroughly so that you are able to create content on the CMS within your time constraints. Such efforts may include, but are not limited to:',
description: 'Heading for list of ways we might be able to assist.',
},
a11yBodyThirdParty: {
id: 'a11yBodyThirdParty',
defaultMessage: 'Purchasing a third-party tool or software for use on an individual basis to assist your use of Studio;',
description: 'Buy third-party software.',
},
a11yBodyContractor: {
id: 'a11yBodyContractor',
defaultMessage: 'Engaging a trained independent contractor to provide real-time visual, verbal and physical assistance; or',
description: 'Hire a contractor.',
},
a11yBodyCodeFix: {
id: 'a11yBodyCodeFix',
defaultMessage: 'Developing new code to implement a technical fix.',
description: 'Make a technical fix.',
},
});
export default messages;

View File

@@ -0,0 +1,146 @@
import React from 'react';
import PropTypes from 'prop-types';
import {
injectIntl, FormattedMessage, intlShape, FormattedDate, FormattedTime,
} from '@edx/frontend-platform/i18n';
import {
ActionRow, Alert, Form, Stack, StatefulButton,
} from '@openedx/paragon';
import { RequestStatus } from '../../data/constants';
import { STATEFUL_BUTTON_STATES } from '../../constants';
import submitAccessibilityForm from '../data/thunks';
import useAccessibility from './hooks';
import messages from './messages';
const AccessibilityForm = ({
accessibilityEmail,
// injected
intl,
}) => {
const {
errors,
values,
isFormFilled,
dispatch,
handleBlur,
handleChange,
hasErrorField,
savingStatus,
} = useAccessibility({ name: '', email: '', message: '' }, intl);
const formFields = [
{
label: intl.formatMessage(messages.accessibilityPolicyFormEmailLabel),
name: 'email',
value: values.email,
},
{
label: intl.formatMessage(messages.accessibilityPolicyFormNameLabel),
name: 'name',
value: values.name,
},
{
label: intl.formatMessage(messages.accessibilityPolicyFormMessageLabel),
name: 'message',
value: values.message,
},
];
const createButtonState = {
labels: {
default: intl.formatMessage(messages.accessibilityPolicyFormSubmitLabel),
pending: intl.formatMessage(messages.accessibilityPolicyFormSubmittingFeedbackLabel),
},
disabledStates: [STATEFUL_BUTTON_STATES.pending],
};
const handleSubmit = () => {
dispatch(submitAccessibilityForm(values));
};
const start = new Date('Mon Jan 29 2018 13:00:00 GMT (UTC)');
const end = new Date('Fri Feb 2 2018 21:00:00 GMT (UTC)');
return (
<>
<h2 className="my-4">
<FormattedMessage {...messages.accessibilityPolicyFormHeader} />
</h2>
{savingStatus === RequestStatus.SUCCESSFUL && (
<Alert variant="success">
<Stack gap={2}>
<div className="mb-2">
<FormattedMessage {...messages.accessibilityPolicyFormSuccess} />
</div>
<div>
<FormattedMessage
{...messages.accessibilityPolicyFormSuccessDetails}
values={{
day_start: (<FormattedDate value={start} weekday="long" />),
time_start: (<FormattedTime value={start} timeZoneName="short" />),
day_end: (<FormattedDate value={end} weekday="long" />),
time_end: (<FormattedTime value={end} timeZoneName="short" />),
}}
/>
</div>
</Stack>
</Alert>
)}
{savingStatus === RequestStatus.FAILED && (
<Alert variant="danger">
<div data-testid="rate-limit-alert">
<FormattedMessage
{...messages.accessibilityPolicyFormErrorHighVolume}
values={{
emailLink: <a href={`mailto:${accessibilityEmail}`}>{accessibilityEmail}</a>,
}}
/>
</div>
</Alert>
)}
<Form>
{formFields.map((field) => (
<Form.Group size="sm" key={field.label}>
<Form.Control
value={field.value}
name={field.name}
isInvalid={hasErrorField(field.name)}
type={field.name === 'email' ? 'email' : null}
as={field.name === 'message' ? 'textarea' : 'input'}
onChange={handleChange}
onBlur={handleBlur}
floatingLabel={field.label}
/>
{hasErrorField(field.name) && (
<Form.Control.Feedback type="invalid" data-testid={`error-feedback-${field.name}`}>
{errors[field.name]}
</Form.Control.Feedback>
)}
</Form.Group>
))}
</Form>
<ActionRow>
<StatefulButton
key="save-button"
onClick={handleSubmit}
disabled={!isFormFilled}
state={
savingStatus === RequestStatus.IN_PROGRESS
? STATEFUL_BUTTON_STATES.pending
: STATEFUL_BUTTON_STATES.default
}
{...createButtonState}
/>
</ActionRow>
</>
);
};
AccessibilityForm.propTypes = {
accessibilityEmail: PropTypes.string.isRequired,
// injected
intl: intlShape.isRequired,
};
export default injectIntl(AccessibilityForm);

View File

@@ -0,0 +1,164 @@
import {
render,
act,
screen,
} from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { initializeMockApp } from '@edx/frontend-platform';
import MockAdapter from 'axios-mock-adapter';
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
import { AppProvider } from '@edx/frontend-platform/react';
import { IntlProvider } from '@edx/frontend-platform/i18n';
import initializeStore from '../../store';
import { RequestStatus } from '../../data/constants';
import AccessibilityForm from './index';
import { getZendeskrUrl } from '../data/api';
import messages from './messages';
let axiosMock;
let store;
const defaultProps = {
accessibilityEmail: 'accessibilityTest@test.com',
};
const initialState = {
accessibilityPage: {
savingStatus: '',
},
};
const renderComponent = () => {
render(
<IntlProvider locale="en">
<AppProvider store={store}>
<AccessibilityForm {...defaultProps} />
</AppProvider>
</IntlProvider>,
);
};
describe('<AccessibilityPolicyForm />', () => {
beforeEach(async () => {
initializeMockApp({
authenticatedUser: {
userId: 3,
username: 'abc123',
administrator: false,
roles: [],
},
});
store = initializeStore(initialState);
axiosMock = new MockAdapter(getAuthenticatedHttpClient());
});
describe('renders', () => {
beforeEach(() => {
renderComponent();
});
it('correct number of form fields', () => {
const formSections = screen.getAllByRole('textbox');
const formButton = screen.getByText(messages.accessibilityPolicyFormSubmitLabel.defaultMessage);
expect(formSections).toHaveLength(3);
expect(formButton).toBeVisible();
});
it('hides StatusAlert on initial load', () => {
expect(screen.queryAllByRole('alert')).toHaveLength(0);
});
});
describe('statusAlert', () => {
let formSections;
let submitButton;
beforeEach(async () => {
renderComponent();
formSections = screen.getAllByRole('textbox');
await act(async () => {
userEvent.type(formSections[0], 'email@email.com');
userEvent.type(formSections[1], 'test name');
userEvent.type(formSections[2], 'feedback message');
});
submitButton = screen.getByText(messages.accessibilityPolicyFormSubmitLabel.defaultMessage);
});
it('shows correct success message', async () => {
axiosMock.onPost(getZendeskrUrl()).reply(200);
await act(async () => {
userEvent.click(submitButton);
});
const { savingStatus } = store.getState().accessibilityPage;
expect(savingStatus).toEqual(RequestStatus.SUCCESSFUL);
expect(screen.getAllByRole('alert')).toHaveLength(1);
expect(screen.getByText(messages.accessibilityPolicyFormSuccess.defaultMessage)).toBeVisible();
formSections.forEach(input => {
expect(input.value).toBe('');
});
});
it('shows correct rate limiting message', async () => {
axiosMock.onPost(getZendeskrUrl()).reply(429);
await act(async () => {
userEvent.click(submitButton);
});
const { savingStatus } = store.getState().accessibilityPage;
expect(savingStatus).toEqual(RequestStatus.FAILED);
expect(screen.getAllByRole('alert')).toHaveLength(1);
expect(screen.getByTestId('rate-limit-alert')).toBeVisible();
formSections.forEach(input => {
expect(input.value).not.toBe('');
});
});
});
describe('input validation', () => {
let formSections;
let submitButton;
beforeEach(async () => {
renderComponent();
formSections = screen.getAllByRole('textbox');
await act(async () => {
userEvent.type(formSections[0], 'email@email.com');
userEvent.type(formSections[1], 'test name');
userEvent.type(formSections[2], 'feedback message');
});
submitButton = screen.getByText(messages.accessibilityPolicyFormSubmitLabel.defaultMessage);
});
it('adds validation checking on each input field', async () => {
await act(async () => {
userEvent.clear(formSections[0]);
userEvent.clear(formSections[1]);
userEvent.clear(formSections[2]);
});
const emailError = screen.getByTestId('error-feedback-email');
expect(emailError).toBeVisible();
const fullNameError = screen.getByTestId('error-feedback-email');
expect(fullNameError).toBeVisible();
const messageError = screen.getByTestId('error-feedback-message');
expect(messageError).toBeVisible();
});
it('sumbit button is disabled when trying to submit with all empty fields', async () => {
await act(async () => {
userEvent.clear(formSections[0]);
userEvent.clear(formSections[1]);
userEvent.clear(formSections[2]);
userEvent.click(submitButton);
});
expect(submitButton.closest('button')).toBeDisabled();
});
});
});

View File

@@ -0,0 +1,58 @@
import { useEffect, useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { useFormik } from 'formik';
import * as Yup from 'yup';
import { RequestStatus } from '../../data/constants';
import messages from './messages';
const useAccessibility = (initialValues, intl) => {
const dispatch = useDispatch();
const savingStatus = useSelector(state => state.accessibilityPage.savingStatus);
const [isFormFilled, setFormFilled] = useState(false);
const validationSchema = Yup.object().shape({
name: Yup.string().required(
intl.formatMessage(messages.accessibilityPolicyFormValidName),
),
email: Yup.string()
.email(intl.formatMessage(messages.accessibilityPolicyFormValidEmail))
.required(intl.formatMessage(messages.accessibilityPolicyFormValidEmail)),
message: Yup.string().required(
intl.formatMessage(messages.accessibilityPolicyFormValidMessage),
),
});
const {
values, errors, touched, handleChange, handleBlur, handleReset,
} = useFormik({
initialValues,
enableReinitialize: true,
validateOnBlur: false,
validationSchema,
});
useEffect(() => {
setFormFilled(Object.values(values).every((i) => i));
}, [values]);
useEffect(() => {
if (savingStatus === RequestStatus.SUCCESSFUL) {
handleReset();
}
}, [savingStatus]);
const hasErrorField = (fieldName) => !!errors[fieldName] && !!touched[fieldName];
return {
errors,
values,
isFormFilled,
dispatch,
handleBlur,
handleChange,
hasErrorField,
savingStatus,
};
};
export default useAccessibility;

View File

@@ -0,0 +1,3 @@
import AccessibilityForm from './AccessibilityForm';
export default AccessibilityForm;

View File

@@ -0,0 +1,76 @@
import { defineMessages } from '@edx/frontend-platform/i18n';
const messages = defineMessages({
accessibilityPolicyFormEmailLabel: {
id: 'accessibilityPolicyFormEmailLabel',
defaultMessage: 'Email Address',
description: 'Label for the email form field',
},
accessibilityPolicyFormErrorHighVolume: {
id: 'accessibilityPolicyFormErrorHighVolume',
defaultMessage: 'We are currently experiencing high volume. Try again later today or send an email message to {emailLink}.',
description: 'Error message when site is experiencing high volume that will include an email link',
},
accessibilityPolicyFormErrorMissingFields: {
id: 'accessibilityPolicyFormErrorMissingFields',
defaultMessage: 'Make sure to fill in all fields.',
description: 'Error message to instruct user to fill in all fields',
},
accessibilityPolicyFormHeader: {
id: 'accessibilityPolicyFormHeader',
defaultMessage: 'Studio Accessibility Feedback',
description: 'The heading for the form',
},
accessibilityPolicyFormMessageLabel: {
id: 'accessibilityPolicyFormMessageLabel',
defaultMessage: 'Message',
description: 'Label for the message form field',
},
accessibilityPolicyFormNameLabel: {
id: 'accessibilityPolicyFormNameLabel',
defaultMessage: 'Name',
description: 'Label for the name form field',
},
accessibilityPolicyFormSubmitAria: {
id: 'accessibilityPolicyFormSubmitAria',
defaultMessage: 'Submit Accessibility Feedback Form',
description: 'Detailed aria-label for the submit button',
},
accessibilityPolicyFormSubmitLabel: {
id: 'accessibilityPolicyFormSubmitLabel',
defaultMessage: 'Submit',
description: 'General label for the submit button',
},
accessibilityPolicyFormSubmittingFeedbackLabel: {
id: 'accessibilityPolicyFormSubmittingFeedbackLabel',
defaultMessage: 'Submitting',
description: 'Loading message while form feedback is being submitted',
},
accessibilityPolicyFormSuccess: {
id: 'accessibilityPolicyFormSuccess',
defaultMessage: 'Thank you for contacting edX!',
description: 'Simple thank you message when form submission is successful',
},
accessibilityPolicyFormSuccessDetails: {
id: 'accessibilityPolicyFormSuccessDetails',
defaultMessage: 'Thank you for your feedback regarding the accessibility of Studio. We typically respond within one business day ({day_start} to {day_end}, {time_start} to {time_end}).',
description: 'Detailed thank you message when form submission is successful',
},
accessibilityPolicyFormValidEmail: {
id: 'accessibilityPolicyFormValidEmail',
defaultMessage: 'Enter a valid email address.',
description: 'Error message for when an invalid email is entered into the form',
},
accessibilityPolicyFormValidMessage: {
id: 'accessibilityPolicyFormValidMessage',
defaultMessage: 'Enter a message.',
description: 'Error message an invalid message is entered into the form',
},
accessibilityPolicyFormValidName: {
id: 'accessibilityPolicyFormValidName',
defaultMessage: 'Enter a name.',
description: 'Error message an invalid name is entered into the form',
},
});
export default messages;

View File

@@ -0,0 +1,42 @@
import React from 'react';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { Helmet } from 'react-helmet';
import { Container } from '@openedx/paragon';
import { StudioFooter } from '@edx/frontend-component-footer';
import Header from '../header';
import messages from './messages';
import AccessibilityBody from './AccessibilityBody';
import AccessibilityForm from './AccessibilityForm';
const AccessibilityPage = ({
// injected
intl,
}) => {
const communityAccessibilityLink = 'https://www.edx.org/accessibility';
const email = 'accessibility@edx.org';
return (
<>
<Helmet>
<title>
{intl.formatMessage(messages.pageTitle, {
siteName: process.env.SITE_NAME,
})}
</title>
</Helmet>
<Header isHiddenMainMenu />
<Container size="xl" classNamae="px-4">
<AccessibilityBody {...{ email, communityAccessibilityLink }} />
<AccessibilityForm accessibilityEmail={email} />
</Container>
<StudioFooter />
</>
);
};
AccessibilityPage.propTypes = {
// injected
intl: intlShape.isRequired,
};
export default injectIntl(AccessibilityPage);

View File

@@ -0,0 +1,46 @@
import {
render,
screen,
} from '@testing-library/react';
import { AppProvider } from '@edx/frontend-platform/react';
import { IntlProvider } from '@edx/frontend-platform/i18n';
import { initializeMockApp } from '@edx/frontend-platform';
import initializeStore from '../store';
import AccessibilityPage from './index';
const initialState = {
accessibilityPage: {
status: {},
},
};
let store;
const renderComponent = () => {
render(
<IntlProvider locale="en">
<AppProvider store={store}>
<AccessibilityPage />
</AppProvider>
</IntlProvider>,
);
};
describe('<AccessibilityPolicyPage />', () => {
describe('renders', () => {
beforeEach(async () => {
initializeMockApp({
authenticatedUser: {
userId: 3,
username: 'abc123',
administrator: false,
roles: [],
},
});
store = initializeStore(initialState);
});
it('contains the policy body', () => {
renderComponent();
expect(screen.getByText('Individualized Accessibility Process for Course Creators')).toBeVisible();
});
});
});

View File

@@ -0,0 +1,28 @@
import { ensureConfig, getConfig } from '@edx/frontend-platform';
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
ensureConfig([
'STUDIO_BASE_URL',
], 'Course Apps API service');
export const getApiBaseUrl = () => getConfig().STUDIO_BASE_URL;
export const getZendeskrUrl = () => `${getApiBaseUrl()}/zendesk_proxy/v0`;
/**
* Posts the form data to zendesk endpoint
* @param {string} courseId
* @returns {Promise<[{}]>}
*/
export async function postAccessibilityForm({ name, email, message }) {
const data = {
name,
tags: ['studio_a11y'],
email: {
from: email,
subject: 'Studio Accessibility Request',
message,
},
};
await getAuthenticatedHttpClient().post(getZendeskrUrl(), data);
}

View File

@@ -0,0 +1,23 @@
/* eslint-disable no-param-reassign */
import { createSlice } from '@reduxjs/toolkit';
const slice = createSlice({
name: 'accessibilityPage',
initialState: {
savingStatus: '',
},
reducers: {
updateSavingStatus: (state, { payload }) => {
state.savingStatus = payload.status;
},
},
});
export const {
updateLoadingStatus,
updateSavingStatus,
} = slice.actions;
export const {
reducer,
} = slice;

View File

@@ -0,0 +1,22 @@
import { RequestStatus } from '../../data/constants';
import { postAccessibilityForm } from './api';
import { updateSavingStatus } from './slice';
function submitAccessibilityForm({ email, name, message }) {
return async (dispatch) => {
dispatch(updateSavingStatus({ status: RequestStatus.IN_PROGRESS }));
try {
await postAccessibilityForm({ email, name, message });
dispatch(updateSavingStatus({ status: RequestStatus.SUCCESSFUL }));
} catch (error) {
if (error.response && error.response.status === 429) {
dispatch(updateSavingStatus({ status: RequestStatus.FAILED }));
} else {
dispatch(updateSavingStatus({ status: RequestStatus.SUCCESSFUL }));
}
}
};
}
export default submitAccessibilityForm;

View File

@@ -0,0 +1,3 @@
import AccessibilityPage from './AccessibilityPage';
export default AccessibilityPage;

View File

@@ -0,0 +1,10 @@
import { defineMessages } from '@edx/frontend-platform/i18n';
const messages = defineMessages({
pageTitle: {
id: 'course-authoring.import.page.title',
defaultMessage: 'Studio Accessibility Policy| {siteName}',
},
});
export default messages;

View File

@@ -3,8 +3,8 @@ import PropTypes from 'prop-types';
import { useDispatch, useSelector } from 'react-redux';
import {
Container, Button, Layout, StatefulButton, TransitionReplace,
} from '@edx/paragon';
import { CheckCircle, Info, Warning } from '@edx/paragon/icons';
} from '@openedx/paragon';
import { CheckCircle, Info, Warning } from '@openedx/paragon/icons';
import { FormattedMessage, injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import Placeholder from '@edx/frontend-lib-content-components';

View File

@@ -1,6 +1,6 @@
import React from 'react';
import PropTypes from 'prop-types';
import { ActionRow, AlertModal, Button } from '@edx/paragon';
import { ActionRow, AlertModal, Button } from '@openedx/paragon';
import { FormattedMessage, injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import ModalErrorListItem from './ModalErrorListItem';

View File

@@ -1,7 +1,7 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Alert, Icon } from '@edx/paragon';
import { Error } from '@edx/paragon/icons';
import { Alert, Icon } from '@openedx/paragon';
import { Error } from '@openedx/paragon/icons';
import { capitalize } from 'lodash';
import { transformKeysToCamelCase } from '../../utils';

View File

@@ -7,8 +7,8 @@ import {
IconButton,
ModalPopup,
useToggle,
} from '@edx/paragon';
import { InfoOutline, Warning } from '@edx/paragon/icons';
} from '@openedx/paragon';
import { InfoOutline, Warning } from '@openedx/paragon/icons';
import PropTypes from 'prop-types';
import { capitalize } from 'lodash';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
@@ -71,7 +71,7 @@ const SettingCard = ({
iconAs={Icon}
alt={intl.formatMessage(messages.helpButtonText)}
variant="primary"
className=" ml-1 mr-2"
className="flex-shrink-0 ml-1 mr-2"
/>
<ModalPopup
hasArrow

View File

@@ -0,0 +1,57 @@
import { Helmet } from 'react-helmet';
import PropTypes from 'prop-types';
import Placeholder from '@edx/frontend-lib-content-components';
import { RequestStatus } from '../data/constants';
import Loading from '../generic/Loading';
import useCertificates from './hooks/useCertificates';
import CertificateWithoutModes from './certificate-without-modes/CertificateWithoutModes';
import EmptyCertificatesWithModes from './empty-certificates-with-modes/EmptyCertificatesWithModes';
import CertificatesList from './certificates-list/CertificatesList';
import CertificateCreateForm from './certificate-create-form/CertificateCreateForm';
import CertificateEditForm from './certificate-edit-form/CertificateEditForm';
import { MODE_STATES } from './data/constants';
import MainLayout from './layout/MainLayout';
const MODE_COMPONENTS = {
[MODE_STATES.noModes]: CertificateWithoutModes,
[MODE_STATES.noCertificates]: EmptyCertificatesWithModes,
[MODE_STATES.create]: CertificateCreateForm,
[MODE_STATES.view]: CertificatesList,
[MODE_STATES.editAll]: CertificateEditForm,
};
const Certificates = ({ courseId }) => {
const {
certificates, componentMode, isLoading, loadingStatus, pageHeadTitle, hasCertificateModes,
} = useCertificates({ courseId });
if (isLoading) {
return <Loading />;
}
if (loadingStatus === RequestStatus.DENIED) {
return (
<div className="row justify-content-center m-6" data-testid="request-denied-placeholder">
<Placeholder />
</div>
);
}
const ModeComponent = MODE_COMPONENTS[componentMode] || MODE_COMPONENTS[MODE_STATES.noModes];
return (
<>
<Helmet><title>{pageHeadTitle}</title></Helmet>
<MainLayout courseId={courseId} showHeaderButtons={hasCertificateModes && certificates?.length > 0}>
<ModeComponent courseId={courseId} />
</MainLayout>
</>
);
};
Certificates.propTypes = {
courseId: PropTypes.string.isRequired,
};
export default Certificates;

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