Compare commits

...

803 Commits

Author SHA1 Message Date
Brayan Cerón
e0ec87c969 fix: find proper courses when searching (backport) (#1496) (#1497)
When active/archived filters were on or there was selected any order filter, the search skipped these values and it was just returned the courses list without the respective filters. Additionally, when a search keyword was applied and a filter was selected, the keyword stayed stuck and the search list returned were not the appropriate
2024-12-09 14:33:48 -08:00
Chris Chávez
4835f72f2c fix: Update error messages when adding user to library (backport) (#1543) (#1550)
Updates the message error when the user doesn't exist when adding a new team member to a library.
2024-12-09 14:23:09 -08:00
Daniel Valenzuela
3ab329d373 fix: avoid changing url when removing filters (#1530) (#1551)
* Makes the Active Tab Key independent from the URL, except for the initial load, where the active tab is set from the url.
*Avoids unnecessarily changing SearchParams: Due to a limitation of the useSearchParams react hook, which uses a memoized value for the URL that becomes stale after selecting a tab, it unexpectedly changes the URL value. Unfortunately there's no way to completely avoid this, so if there's a usageKey url param, the hook setter function will be called and the URL will revert to the stale memoized url.
2024-12-09 11:20:52 -05:00
Rômulo Penido
7c97ffecb5 fix: show/hide "new library" button based on separate v1/v2 permissions (backport) (#1549) 2024-12-06 12:12:11 -08:00
Chris Chávez
90727590dd fix: Show published OLX in Library Content Picker (backport) (#1534) (#1546) 2024-12-06 11:48:52 -08:00
Rômulo Penido
1c82a67364 fix: editor flicker after creating xblock (#1529) 2024-11-26 14:41:14 -08:00
Navin Karkera
d08ef83659 fix: remove unnecessary toast notification on adding component (#1490) (#1528)
(cherry picked from commit 033acc45f1)
2024-11-22 11:15:01 -08:00
Chris Chávez
13bce7e034 fix: Show published count component in library content picker (#1481) (#1521)
When using the library component picker, show the correct number on component count (published components) in collection cards.
2024-11-21 15:01:48 -08:00
Chris Chávez
54888d03bc fix: TinyMce aux modal issues in text editors (#1500) (#1520)
The following bugs were found with the TinyMCE aux modal (used in emoticons, formulas and embed iframe):

* The TinyMCE aux modal and the Editor modal close when clicking on any content in the aux modal.
* When the user opens the Edit Source Code modal, this adds data-focus-on-hidden to the TinyMce aux modal, making it unusable (not clickable).
* Since they are two separate modals, the focus remains on the editor modal, making it impossible to use scrolling or inputs from the modal aux.

Solution: Move the aux modal inside the editor modal.

One discarded solution: Block the modal editor from closing when interacting with the modal aux. The modal editor still retained focus.
2024-11-19 15:32:47 -08:00
Daniel Valenzuela
e6d9f3a50d fix: simplify Library Home Page (v2) (#1443) (#1495) 2024-11-18 14:46:25 -08:00
Navin Karkera
74b455287e feat: show info banner in component picker (#1498) (#1501)
Displays a infor banner if only published content is visible in component picker.

(cherry picked from commit efd2b3d27d)
2024-11-14 12:00:02 -05:00
Jillian
e2adb45493 fix: show a more detailed error on Bad Request (#1468) (#1478)
Show a detailed error when 400 Bad Request received while adding a component to a library, either a new or pasted component. The most likely error from the backend here is "library can only have {max} components", and since this error is translated already, we can just report it through.

(cherry picked from commit f1bdc6200f)
2024-11-06 22:42:31 -05:00
Rômulo Penido
d4e9a6bec2 fix: add spacing to searchbar and simplify render conditions (#1476)
Adds padding between the search bar and the library list.

Also, the render method was refactored to be a bit simpler.

Backport of #1461
2024-11-06 22:05:57 -05:00
Navin Karkera
e6741496dc fix: add component to collection on paste [FC-0062] (#1450) (#1472)
Link component to collection if pasted in a collection page.
Fixes: https://github.com/openedx/frontend-app-authoring/issues/1435

(cherry picked from commit 549dbaa0fa)
2024-11-06 21:56:44 -05:00
Navin Karkera
9304a83bef chore: hide transcripts in video preview for library (#1459) (#1474)
Fixes: #1453
(cherry picked from commit e118eb5971)
2024-11-06 21:47:05 -05:00
Navin Karkera
3173f41e63 feat: handle unsaved changes in text & problem editors (#1444) (#1471)
The text & problem xblock editors will display a confirmation box before
cancelling only if user has changed something else it will directly go
back.

(cherry picked from commit df8a65dc4e)
2024-11-06 11:26:12 -05:00
Jillian
866dd9bd31 fix: Hide / error on Libraries v2 pages if !librariesV2Enabled (#1449) (#1473)
Show an error message if the user tries to view a v2 Library while Libraries V2 are disabled in the platform.

(cherry picked from commit d7bbd40de1)
2024-11-06 11:25:30 -05:00
Rômulo Penido
f10ad9f525 fix: enable publish button on library after component edit [sumac] [FC-0062] (#1447)
This PR fixes the following bug: After publishing a library then editing a component, the "Publish" button in Library Info doesn't become enabled until you refresh
Fixes: https://github.com/openedx/frontend-app-authoring/issues/1455
Backport: https://github.com/openedx/frontend-app-authoring/pull/1446
2024-11-04 11:56:20 -05:00
Chris Chávez
81d78b9613 fix: Library Preview Expand button covers dropdown (#1438) (#1442) 2024-10-30 09:03:00 -05:00
Rômulo Penido
4886df7d6f [sumac] fix: empty state for library selection on component picker [FC-0062] (#1441)
This PR fixes the empty state text for adding library content if the user can't access any library.
2024-10-28 18:49:58 -05:00
Jillian
62dfb75169 fix: use absolute URL for Export Tags menu item
Use absolute URL for Export Tags menu item so that the menu item works no matter where in the course it's used. Fix this issue: https://github.com/openedx/frontend-app-authoring/issues/1380

(cherry picked from commit 774728a9c0)
2024-10-25 21:50:04 -05:00
Braden MacDonald
3d8d248599 feat: arbitrary asset upload/deletion for Library Components [FC-0062] (#1430)
Allow users to upload and delete assets associated with Content Library
components via the sidebar panel, under the "Advanced Details" section
of the "Details" tab. This is intended as a debug tool and power-user
feature, similar to the OLX editor provided there. It's also serving as
our interim image-upload solution, because it was easier to implement
than the full modal that integrates with TinyMCE.

---------

Co-authored-by: XnpioChV <xnpiochv@gmail.com>
2024-10-24 09:46:27 -04:00
Cristhian Garcia
e1ce3eb484 fix: display image preview in libraries editor (#1403)
Prior to this commit, the TinyMCE editor image preview only worked in
courses, and did not work for content libraries.
2024-10-24 09:39:37 -04:00
Rômulo Penido
a8aa495542 feat: add existing content to a collection [FC-0062] (#1416)
Allows library components to be added to a collection using the add-content
sidebar. For the Libraries Relaunch Beta.

For: https://github.com/openedx/frontend-app-authoring/issues/1173
2024-10-23 11:49:46 -04:00
Navin Karkera
c0c74dec83 feat: show confirmation dialog before discarding library changes [FC-0062] (#1428)
* feat: show confirmation dialog before discarding library changes
2024-10-23 10:47:23 -05:00
Navin Karkera
f67c3ffc4c feat: direct link to single block in library [FC-0062] (#1392)
* feat: direct link to single block in library

Adds support for displaying single xblock in a library when passed a
query param: usageKey. This is required for directing users to a
specific block from course.

* feat: show alert while editing library block from course
2024-10-23 10:25:54 -05:00
Rômulo Penido
11470f256d feat: library component picker now supports multi-select (#1417) 2024-10-22 16:41:49 -07:00
Brian Smith
fe37d119f2 feat(deps): update header to 5.6.0 (#1424) 2024-10-22 19:18:50 -04:00
Braden MacDonald
8c8d9119d4 fix: don't revert to advanced editor if problem contains fields like url_name (#1421) 2024-10-22 18:53:22 +00:00
Chris Chávez
21cbf80f23 feat: Show published components on content picker (#1420)
* feat: Show published components on content picker

---------

Co-authored-by: Braden MacDonald <braden@opencraft.com>
2024-10-22 13:47:07 -05:00
Daniel Valenzuela
966e1c3d91 feat: publish single library component (#1407) 2024-10-22 17:31:17 +00:00
Braden MacDonald
57e7baf59e fix: this repo has been renamed to frontend-app-authoring (#1419) 2024-10-22 10:19:24 -07:00
Navin Karkera
675e02fcbd feat: "add to collection" menu item functionality (#1413) 2024-10-22 09:49:51 -07:00
Braden MacDonald
841aede8cd perf: don't load advanced info details (library components) before they're needed (#1409) 2024-10-21 14:07:28 -07:00
Braden MacDonald
6ae68bd122 feat: Menu option to delete a component + small fixes (#1408)
* feat: menu option to delete a component
* feat: close component sidebar if it's open when that component id deleted
* feat: hide unsupported block types from the "Add Content" menu
* fix: expand and internationalize the "component usage" text
2024-10-21 14:04:45 -07:00
Navin Karkera
d49fc85163 refactor: remove parentLocator and next button from lib component picker (#1412) 2024-10-21 10:05:04 -07:00
Navin Karkera
56e025a4f0 refactor: lib component picker modal to only post message with block info (#1401) 2024-10-19 11:55:25 -07:00
Max Sokolski
a94df2fdf0 fix: set original value for TypeaheadDropdown component 2024-10-18 16:35:53 -03:00
Jillian
cfe19894d1 feat: Let Studio Home REST API determine if libraries v1 and/or v2 are enabled (#1329) 2024-10-18 12:03:26 -07:00
Braden MacDonald
40a6ee9ca5 feat: View for comparing published version of library block to previous (#1393) 2024-10-18 11:27:10 -07:00
Chris Chávez
4facf1cf5d feat: add tags to collections [FC-0062] (#1379)
* feat: Add ContentTagsDrawer to collection

* test: Add test to show ContentTagsDrawer on CollectionInfo
2024-10-16 21:50:17 -05:00
Rômulo Penido
b81f611a0e feat: add library component picker (#1356) 2024-10-16 10:18:12 -07:00
Maria Grimaldi
8a4d1f4810 refactor!: turn on homepage course API V2 consumption by default (#1307)
* refactor!: turn on homepage course API V2 consumption by default

Turn on getting courses from the HomePageCourses API
which allows pagination, filtering and ordering. See https://github.com/openedx/edx-platform/pull/34173
for more details on the API implementation.

* fix: home page initial a-z course sort

---------

Co-authored-by: Diana Olarte <diana.olarte@edunext.co>
2024-10-16 12:29:20 -04:00
David Ormsbee
1bdea093b0 fix: disable static asset mangling for v2 Content Libraries
The static asset substitution used to make images show up properly when
in the TinyMCE editor doesn't work for Content Libraries. Unfortunately,
this will cause the static asset references in XBlock content to get
mangled and saved incorrectly. So until we can handle it correctly,
we're just going to disable it entirely if the LearningContext is a v2
Content Library.

This means that static assets won't display properly in the editor
itself, but it should at least get written/preserved correctly, so that
those assets will show up properly in XBlock previews.
2024-10-16 11:54:19 -04:00
David Ormsbee
a1181f3d49 refactor: switch Content Library XBlock preview to Studio
edx-platform commit 7316111 (PR #35598) moved the XBlock embed view so
that it can be rendered on either LMS or Studio. This commit moves the
frontend to actually call the Studio endpoint. This will make Content
Library static asset display easier, because that view will only be made
available through Studio and not the LMS.
2024-10-16 11:54:19 -04:00
Stanislav
ba8e3d448e fix: Calendar icon over datepicker modal (#1365) 2024-10-16 10:20:27 -04:00
Jillian
1ee3229104 feat: UI to manage users/permissions for the content libraries (#1362) 2024-10-15 17:24:00 +00:00
Navin Karkera
84487602cc feat: manage collections in component sidebar [FC-0062] (#1373)
* feat: add to collection in sidebar

* feat: manage collections

* test: add tests for manage collections

* feat: remove from collection menu option
2024-10-15 10:20:23 -05:00
Navin Karkera
7fb460019e feat: add an allowlist of for supported blocks in library [FC-0062] (#1378)
* feat: show error msg from server on paste

* feat: add an allowlist of for supported blocks in library

Libraries v2 currently don't support editing blocks other than problem,
text and videos. This commit adds a configuration variable called
`LIBRARY_SUPPORTED_BLOCKS` to setup allowed list of block types users
can paste into libraries. By default it is set to support
'problem,text,video,html`.

* feat: enable add button for blocks based on setting


---------

Co-authored-by: Rômulo Penido <romulo@opencraft.com>
2024-10-15 09:52:35 -05:00
Braden MacDonald
66b14a5b16 docs: update the README with an easy way to run the MFE on your host (#1364) 2024-10-10 14:02:42 -07:00
ABBOUD Moncef
3696836de6 feat: save discussion alert dismissal (#1245) 2024-10-09 16:23:12 -04:00
Navin Karkera
434fea3a95 feat: delete collection [FC-0062] (#1333)
* feat: delete collection

* feat: update button status on delete

* test: add tests for collection delete
2024-10-08 16:59:06 +00:00
Braden MacDonald
75f937e11a feat: Libraries v2: Advanced Component Info & OLX Editor (#1346) 2024-10-08 09:41:21 -07:00
Rômulo Penido
85b5730114 fix: change collection details component slots (#1363) 2024-10-07 21:34:48 -05:00
Braden MacDonald
8c125df9aa feat: Open Editors in a Modal (library components only) [FC-0062] (#1357)
* feat: allow opening editors in modals

* refactor: add an EditorContext

* test: update tests accordingly

* test: make testUtils call clearAllMocks() automatically :)
2024-10-07 21:04:49 -05:00
edX requirements bot
83322e2052 chore: update browserslist DB (#1367)
Co-authored-by: abdullahwaheed <42172960+abdullahwaheed@users.noreply.github.com>
2024-10-07 10:15:40 -07:00
dependabot[bot]
b6eeec8e60 build(deps): bump actions/checkout from 3 to 4 (#1371) 2024-10-07 10:12:16 -07:00
Jillian
b957f3b4e3 Use block type label instead of Library block_types REST API [FC-0062] (#1361)
* style: avoid using reserved word "type" as variable name

use componentType or blockType instead.

* refactor: let BlockTypeLabel handle displaying the component label

including the child count, if one is provided.

This change removes hooks for the block_types REST API

* test: add tests for BlockTypeLabel

---------

Co-authored-by: Chris Chávez <xnpiochv@gmail.com>
2024-10-04 13:04:23 -05:00
Jillian
9c1fd5a68c fix: Show spinner while loading library components (#1331) 2024-10-03 21:02:32 -07:00
Braden MacDonald
652af9f6a5 refactor: Improve LibraryContext, convert tests to testUtils (#1345) 2024-10-03 19:35:43 -07:00
Jillian
dc6ede4d80 fix: use "other" component type in decide Card header background (#1359)
if no other background color is found
2024-10-03 15:26:12 -07:00
Jesper Hodge
8c6bbb895f fix: update express (#1351)
Just a package update
2024-10-03 15:21:37 -04:00
Braden MacDonald
4d0f92e265 fix: upload codecov report as a separate workflow step (#1355) 2024-10-02 10:49:38 -07:00
Rômulo Penido
0349188c42 feat: allow full width content in library authoring [FC-0062] (#1258)
* feat: allow full width content in library authoring

* chore: update header and footer versions

---------

Co-authored-by: Jillian <jill@opencraft.com>
2024-10-02 06:16:25 -05:00
Rômulo Penido
b1772383f4 fix: component preview modal overflow (#1348) 2024-10-01 16:56:58 -07:00
Kristin Aoki
b71f2148c9 feat: update ora settings to only be flexible peer grading (#1332) 2024-10-01 13:11:52 -04:00
edX requirements bot
e9c10c7f9e chore: update browserslist DB (#1347)
Co-authored-by: abdullahwaheed <42172960+abdullahwaheed@users.noreply.github.com>
2024-09-29 17:35:01 -07:00
Rômulo Penido
4d67e8bda9 feat: improve collection sidebar (#1320)
* feat: improve collection sidebar

* feat: add comments to splice blockTypesArray code

Co-authored-by: Jillian <jill@opencraft.com>
---------

Co-authored-by: Jillian <jill@opencraft.com>
Co-authored-by: Chris Chávez <xnpiochv@gmail.com>
2024-09-27 21:24:12 -05:00
Dmytro
c80483c053 fix: Create button remains deactivated until pick a new org (#1279)
Co-authored-by: Dima Alipov <dimaalipov@192.168.1.101>
2024-09-27 11:37:32 -04:00
Chris Chávez
2cd77ce455 feat: Add tags to manage sidebar of library components (#1299) 2024-09-26 23:33:36 -07:00
Braden MacDonald
95c17537c1 fix: don't revert to advanced problem editor when max_attempts is set (#1326) 2024-09-26 09:50:28 -07:00
Braden MacDonald
3662fadad4 feat!: Remove support for the (deprecated) library authoring MFE (#1327) 2024-09-26 08:38:16 -07:00
Braden MacDonald
ccce44a1c8 fix: library metadata times are actually displayed in local time (#1309) 2024-09-25 17:26:36 -07:00
Rômulo Penido
ff67c9a952 feat: add component Details sidebar [FC-0062] (#1303)
* feat: add ComponentDetails component

---------

Co-authored-by: Jillian <jill@opencraft.com>
2024-09-25 14:33:45 -05:00
renovate[bot]
c13ab00344 chore(deps): update dependency @openedx/frontend-build to v14.1.4 (#1308)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-09-25 10:17:08 -07:00
Rômulo Penido
b6ec5e1e3a fix: remove preview overlay from library component sidebar (#1323) 2024-09-25 09:32:49 -07:00
Braden MacDonald
5f41db83c2 feat: Enable the Video editor in content libraries [FC-0062] (#1319)
* feat: enable video editor in libraries

* fix: a11y issue in video editor - URL and ID fields were combined

* test: tests for video editor
2024-09-24 21:11:06 -05:00
Braden MacDonald
95521d3b8d Cleanups for the video editor [FC-0062] (#1318)
* refactor: cleanups to video editor code

* test: ignore coverage of blank default data
2024-09-24 20:55:15 -05:00
Braden MacDonald
64d718d198 fix: Use soft nav when clicking a library from studio home (#1306) 2024-09-24 09:32:56 -07:00
edX requirements bot
353ef508df chore: update browserslist DB (#1312)
Co-authored-by: abdullahwaheed <42172960+abdullahwaheed@users.noreply.github.com>
2024-09-23 13:22:37 -07:00
renovate[bot]
8b50449c1f fix(deps): update dependency npm to v10.8.3 (#1313)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-09-23 09:38:30 -07:00
Navin Karkera
b7ae82bde2 feat: Collections page (in libraries) (#1281) 2024-09-20 10:15:25 -07:00
renovate[bot]
0d472ae66f chore(deps): update dependency eslint-import-resolver-webpack to v0.13.9 (#1284)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-09-19 13:52:05 -07:00
Zachary Hancock
4e609e02e5 feat: improve error message for proctored exam settings (#1300) 2024-09-19 16:03:34 -04:00
Braden MacDonald
8d49f2ed4e feat: display library title in browser tab (#1305) 2024-09-19 12:34:48 -07:00
Stanislav
f3274e70a6 fix: Fix content overflow in the Pages & Resources modal windows (#1301) 2024-09-19 15:16:53 -04:00
Navin Karkera
9d3a05f1bd feat: show children count in collection card (#1298) 2024-09-19 09:50:38 -07:00
Chris Chávez
053a9b1074 fix: inconsistency with the select all functionality in problem capa type filter (#1294) 2024-09-18 11:35:56 -07:00
Rômulo Penido
fc4b700624 fix: responsiveness on library authoring sidebar (#1293)
* fix: responsiveness on library authoring sidebar

* fix: adjust margin to prevent height change

* fix: prevent button stretch
2024-09-18 11:19:56 -07:00
Braden MacDonald
314dfa60e2 feat: Enable capa problem editor for components in libraries (#1290)
* feat: enable the problem editor for library components

* fix: don't try to load "advanced settings" when editing problem in library

* fix: don't fetch images when editing problem in library

* docs: add a note about plans for the editor modal

* fix: choosing a problem type then cancelling resulted in an error

* chore: remove unused mockApi, clean up problematic 'module' self import

* test: update workflow test to test problem editor

* feat: show capa content summary on cards in library search results

* docs: fix comment typos found in code review

* refactor: add 'key-utils' to consolidate opaque key logic
2024-09-18 17:45:41 +00:00
Braden MacDonald
b01090902a fix: propTypes warnings in Problem Editor, refactor some code to TS (#1280)
* fix: a11y - missing 'alt' text for Problem Editor IconButton
* fix: warning in <ProblemTypeSelect> component - missing key prop in list
* fix: warning: `problemType` required in `ProblemEditor`, but is `null`
* fix: warning: The prop `onClose` marked as required in `SelectTypeModal`
* fix: warning: prop `name` is marked as required in `ForwardRef(_c)`
* fix: warning: props `alt`, `id`, and `key` are required
* test: improve test coverage of SelectTypeModal, refactor some code
* test: improve test coverage
2024-09-18 17:01:56 +00:00
Stanislav
82a3b7c986 fix: Fix content overflow in the Overwrite Files modal window (#1291) 2024-09-17 10:28:18 -04:00
renovate[bot]
fb3533ad49 fix(deps): update dependency frontend-components-tinymce-advanced-plugins to v1.0.4 (#1285)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-09-16 19:34:58 +00:00
Rômulo Penido
dd7e4d4297 feat: add component sidebar manage tab [FC-0062] (#1275) 2024-09-16 14:13:41 -05:00
Kristin Aoki
902853d649 fix: isInitialized selector depends on unitUrl for course blocks (#1288) 2024-09-16 14:34:56 -04:00
Kyle McCormick
6eed6438cb docs: update README based on rename (#1289) 2024-09-16 13:05:12 -04:00
edX requirements bot
644f1706a2 chore: update browserslist DB (#1283)
Co-authored-by: abdullahwaheed <42172960+abdullahwaheed@users.noreply.github.com>
2024-09-16 09:49:11 -07:00
Brayan Cerón
80e3592669 refactor: remove thumbnail for non-edX videos & allow removing fallback URLs (#1241)
* refactor: remove thumbnail from non-edx videos

* fix: when deleting a fallback URL the app crashed

* refactor: simplify conditional rendering
2024-09-16 11:53:05 -04:00
Rômulo Penido
121ced42ec feat: preview components (xblocks) on library authoring pages (#1242) 2024-09-14 10:03:49 -07:00
Chris Chávez
a37a1b1ef8 feat: Create collection Modal [FC-0062] (#1259)
* feat: Enable Collection button on Create Component in Library

* feat: CreateCollectionModal added

* test: For CreateCollectionModal

* refactor: Migrate FormikControl to TypeScript

* test: Add tests for EmptyStates
2024-09-13 21:07:02 -05:00
Braden MacDonald
fd48fef299 feat: edit Text components within content libraries [FC-0062] (#1240) 2024-09-12 19:39:42 -07:00
Navin Karkera
9b61037311 feat: collections tab [FC-0062] (#1257)
* feat: add collections query to search results

* feat: collections tab with basic cards

* feat: add collection card also fix inifinite scroll for collections

* feat: collection empty states

* test: add test for collections card
2024-09-12 18:55:34 -05:00
renovate[bot]
4035931cbb fix(deps): update dependency @openedx/paragon to v22.8.1 (#1268)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-09-12 19:26:42 +00:00
renovate[bot]
e2e3104474 fix(deps): update dependency @edx/frontend-component-footer to v14.0.10 (#1244)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-09-12 19:25:27 +00:00
edX requirements bot
88a038c0ea chore: enable github action auto update in dependabot.yml (#1256) 2024-09-12 15:12:17 -04:00
Demid
45c68d6ca4 Hide "Advanced Settings" settings item [BB-9081] (#1252)
* refactor: convert header utils to hooks

* feat: hide advanced settings button if a user doesn't have access
2024-09-12 14:55:01 -04:00
Dmytro
56728310f4 fix: create course button inactive after using org drop-down (#1276)
Co-authored-by: Dima Alipov <dimaalipov@192.168.1.101>
2024-09-12 12:46:04 -04:00
renovate[bot]
9bbbf610b7 fix(deps): update dependency @edx/openedx-atlas to v0.6.2 (#1228) 2024-09-11 11:30:51 -07:00
Braden MacDonald
6255768c97 test: refactor and fix flakiness of LibraryAuthoringTest (#1263) 2024-09-10 13:34:22 -07:00
Dmytro
513309c160 fix: no validation for combined length of org, number, run (#1262)
Co-authored-by: Dima Alipov <dimaalipov@192.168.1.101>
2024-09-10 10:24:11 -04:00
edX requirements bot
bbe15afbe9 chore: update browserslist DB (#1175)
Co-authored-by: abdullahwaheed <42172960+abdullahwaheed@users.noreply.github.com>
2024-09-09 14:41:08 -07:00
Braden MacDonald
f9c11f8129 feat: Add type stubs for frontend-platform/i18n (#1251) 2024-09-09 09:30:14 -07:00
Braden MacDonald
376f31ebae chore: clean up dependencies (#1255) 2024-09-09 09:27:15 -07:00
renovate[bot]
849471bfed chore(deps): update dependency @openedx/frontend-build to v14.1.2 (#1213) 2024-09-06 13:27:43 -07:00
Kaustav Banerjee
6b10fa7401 feat: remove new library button if user does not have create access for v1 libraries (#1216) 2024-09-06 10:49:38 -07:00
Kyr
3a61e84c50 feat: fixed height for prerequisite course dropdown list (#1154)
Co-authored-by: Kyrylo Hudym-Levkovych <kyr.hudym@kyrs-MacBook-Pro.local>
2024-09-06 10:28:55 -04:00
Kristin Aoki
dcf05cde07 fix: tinymce render outside of editors (#1254) 2024-09-05 13:18:26 -07:00
Chris Chávez
34f0bf5253 fix: UX nits on Library Info Sidebar (#1253) 2024-09-04 10:25:36 -07:00
Rômulo Penido
c4d00017e0 fix: FilterItem key warning (#1246) 2024-09-04 08:51:13 -05:00
Braden MacDonald
735d978894 refactor: Merge frontend-lib-content-components into this repository 2024-08-29 10:19:17 -07:00
Braden MacDonald
9d0898cdfe chore: update with master 2024-08-29 10:03:55 -07:00
Bilal Qamar
1e7e3e7036 build: Upgrade to Node 20 (#1209)
* feat: updated node to v20

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

* refactor: updated lockfile version workflow
2024-08-29 12:08:30 -04:00
Rômulo Penido
48e0ec1f70 feat: add library component sidebar [FC-0062] (#1217) 2024-08-29 07:22:13 -05:00
Braden MacDonald
af2b4dd3cb fix: remove another accidental import of test code from the build 2024-08-28 10:51:36 -07:00
Chris Chávez
64ffaddf3c feat: Capa problem types submenu [FC-0059] (#1207) 2024-08-28 08:16:02 -05:00
Braden MacDonald
7d7394521b chore: update with latest master 2024-08-27 13:45:23 -07:00
Ihor Romaniuk
f36b2183d6 fix: reset active answers for single answer problem type (#426) 2024-08-27 13:44:11 -07:00
Braden MacDonald
83fda560c1 fix: remove accidental import of test code from the build 2024-08-27 13:41:55 -07:00
Chris Chávez
d99a09efba fix: Library v2 info sidebar UI fixes (#1226) 2024-08-26 13:33:54 -07:00
Jillian
259a50c468 UI fixes for Sort Library Component [FC-0059] (#1222)
fix: use "Recently Modified" as the default sort option

When search keyword(s) are entered, use "Most Relevant" as default.

Also

* Hides "Most Relevant" option if no keyword is entered.
* Re-orders the sort menu options
* Ensures the default sort option is not stored in the query string
* Shows the selected sort option on the drop-down toggle button
* Shows "Sort By" as a header inside the drop-down menu
2024-08-23 19:11:53 -05:00
Bilal Qamar
3e0f7b5758 test: Add Node 20 to CI matrix (#1224) 2024-08-22 14:37:36 -04:00
Rômulo Penido
21c9e30207 refactor: change toast component (#1211) 2024-08-22 08:50:43 -05:00
Muhammad Anas
8ae9dfbd88 feat: customize the certificate link in header (#1223)
* feat: customize the certificate link in header

* fix: lint issues

* fix: tests
2024-08-21 10:36:14 -04:00
Navin Karkera
3089d0b993 fix: discard button [FC-0062] (#1214)
* fix: discard changes

* refactor: disable discard btn for new libs

Enable it if components are added.

* refactor: invalidate library related content queries

* chore: add comment about content search query invalidation
2024-08-20 21:12:06 -05:00
renovate[bot]
47cec6e4c9 fix(deps): update dependency @edx/frontend-lib-content-components to v2.6.8 (#1218)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-08-20 15:21:54 -04:00
Rômulo Penido
f370b565c2 fix: dropdown checkbox click area (#1215) 2024-08-17 16:43:27 -07:00
Braden MacDonald
155a710aa8 chore: work around type error with <SelectableBox> 2024-08-17 16:41:12 -07:00
Braden MacDonald
591a02e8a7 chore: update with master 2024-08-17 16:27:00 -07:00
Braden MacDonald
f90bbb2de7 chore: update to frontend-lib-content-components 2.6.8 2024-08-17 16:23:19 -07:00
Braden MacDonald
28e1956708 chore: update imports, fix lint issues 2024-08-17 16:12:58 -07:00
Ihor Romaniuk
b55e5c9f8f fix: answer range validation in Numerical input (#482) 2024-08-16 12:19:11 -04:00
Kyr
a9e8bd5558 fix: license widget checkbox and link (#486)
* fix: share alike after save, license link for creative common

* test: update snapshot

---------

Co-authored-by: Kyrylo Hudym-Levkovych <kyr.hudym@kyrs-MacBook-Pro.local>
2024-08-16 10:19:13 -04:00
Yusuf Musleh
95ac0983a3 feat: Add "Paste from Clipboard" to lib v2 sidebar (#1187) 2024-08-15 10:03:39 -07:00
renovate[bot]
7c59b4a210 fix(deps): update dependency @openedx/paragon to v22.7.0 (#1180)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-08-13 16:39:37 -04:00
renovate[bot]
de3befec08 fix(deps): update dependency @edx/frontend-component-header to v5.3.4 (#1179)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-08-13 18:08:37 +00:00
renovate[bot]
6ff3847c6c fix(deps): update dependency @edx/openedx-atlas to v0.6.1 (#1123)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-08-13 13:53:33 -04:00
renovate[bot]
ea90e7e93c fix(deps): update dependency @edx/frontend-lib-content-components to v2.6.6 (#1210)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-08-13 17:50:42 +00:00
renovate[bot]
48ffa0f970 fix(deps): update dependency @edx/frontend-component-footer to v14.0.8 (#1161)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-08-13 17:49:42 +00:00
Chris Chávez
4f5346ed31 feat: Library info sidebar - allows lib rename+publish (#1138) 2024-08-13 10:37:34 -07:00
Ihor Romaniuk
940482dd9a fix: add validation to problem number fields (#425) 2024-08-12 11:04:28 -04:00
renovate[bot]
8285f8ec5a fix(deps): update dependency @edx/frontend-lib-content-components to v2.6.5 (#1206)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-08-12 09:43:34 -04:00
Braden MacDonald
afa2317131 feat: 'frontend-lib-content-components' into this repo 2024-08-09 11:48:49 -07:00
Braden MacDonald
d3d5fe0e1b Removed unneeded files from lib-content-components 2024-08-09 11:47:35 -07:00
Kristin Aoki
b088a8fe3d fix: asset name parsing in static converter (#501) 2024-08-09 13:02:34 -04:00
Rômulo Penido
bb88101255 feat: add "copy to clipboard" feature to library authoring UI (#1197) 2024-08-08 09:32:04 -07:00
Chris Chávez
a7645afd22 fix: UI fixes for read-only libraries etc. (#1198)
* fix: Hide open Create content buttons without permissions

* feat: Read only badge on library Home

* refactor: library authoring to get canEditLibrary from useContentLibrary

* style: Typo on the code
2024-08-07 18:26:32 -07:00
Kristin Aoki
7379e734a0 feat: replace progress bar with loading spinner (#1192) 2024-08-07 12:07:22 -04:00
Kristin Aoki
3d82d37943 Revert "fix(deps): update dependency @edx/frontend-lib-content-components to …" (#1205)
This reverts commit 6f13164998.
2024-08-06 12:50:21 -04:00
Brandon Bodine
553acd8fcc chore: remove unused ai-translations env vars (#1204) 2024-08-06 07:17:08 -06:00
renovate[bot]
6f13164998 fix(deps): update dependency @edx/frontend-lib-content-components to v2.6.4 (#1184)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-08-05 09:31:12 -04:00
Jorg Are
9efb583cdc feat: replace ai-translations component with a plugin slot (#1186)
* feat: replace ai-translations component with a plugin slot

* feat: move ai-translations enabled check to plugin
2024-08-05 13:46:50 +01:00
Kristin Aoki
b68257e176 fix: remove imports breaking build (#500) 2024-08-02 15:04:49 -04:00
Rômulo Penido
680b5ff160 refactor: convert a couple files to TS and improve typings/tests (#1181)
* refactor: convert files to ts and improve typings/tests

* fix: set return type to unkown for future fix
2024-08-02 09:26:10 -07:00
Kristin Aoki
beb4813c53 fix: setAssetToStaticUrl regex matcher (#497) 2024-08-01 15:56:32 -04:00
Ihor Romaniuk
ce8703799b fix: add rtl support to editor (#424) 2024-08-01 13:21:01 -04:00
Adolfo R. Brandes
5825dd36d3 Merge pull request #487 from open-craft/braden/fix-extra-scss
fix: don't accidentally bundle paragon CSS x2
2024-08-01 09:55:35 -03:00
Jillian
cba85ab96d test: fix flaky library-authoring test (#1193) 2024-07-30 13:33:24 -07:00
Kristin Aoki
cc3bbfd9af fix: package.json out of sync with package-lock.json and upgrade (#1194) 2024-07-30 20:24:37 +00:00
Jillian
4f88948844 feat: adds sort widget to search manager and library component page (#1147) 2024-07-30 09:41:10 -07:00
Yusuf Musleh
699cbeadb3 feat: Add cancel create library button (#1182) 2024-07-26 09:39:24 -07:00
Rômulo Penido
6382898213 chore: add ts* files to lint --fix script 2024-07-26 09:06:54 -07:00
Kristin Aoki
3dfc579745 feat: add conditional for new parser beta testing (#496) 2024-07-26 10:59:40 -04:00
renovate[bot]
649863d094 fix(deps): update dependency @edx/frontend-lib-content-components to v2.5.2 (#1178)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-07-25 11:21:40 -04:00
renovate[bot]
1be693b826 fix(deps): update dependency npm to v10.8.2 2024-07-24 15:02:19 -07:00
renovate[bot]
0933bae314 chore(deps): update actions/checkout action to v4 2024-07-24 14:43:33 -07:00
Rômulo Penido
f159b2b31c chore: update frontend-build (#1155) 2024-07-24 21:42:03 +00:00
Rômulo Penido
25ab1fffa1 feat: adds filter by tags and contentType to library home (#1141)
Refactor to add search-manager feature.

Co-authored-by: Yusuf Musleh <yusuf@opencraft.com>
2024-07-24 12:13:24 -07:00
Milad Emami
ebab15f046 fix: correct typo in Alert component prop (#494)
Corrected the typo in the prop name of the Alert component from 'varaint' to 'variant'. This change ensures the proper functioning of the alert in informational variant.
2024-07-24 13:27:16 -04:00
Chris Chávez
77135cde1d feat: Library v2 components tab (#1109) 2024-07-22 18:04:57 -07:00
edX requirements bot
3a14141a4e chore: update browserslist DB (#1156)
Co-authored-by: abdullahwaheed <42172960+abdullahwaheed@users.noreply.github.com>
2024-07-17 10:23:54 +02:00
Chris Chávez
3d24741062 feat: "Add content" sidebar on each library home page (#1065) 2024-07-17 10:15:40 +02:00
Rômulo Penido
e087001905 feat: create library (v2) form (#1116) 2024-07-12 05:54:37 -07:00
Braden MacDonald
cc41a2fda1 fix: source map warning seen during build (#1150) 2024-07-11 15:15:02 -04:00
renovate[bot]
5dee203401 fix(deps): update dependency @edx/frontend-lib-content-components to v2.5.1 (#1153)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-07-11 09:55:51 -04:00
Kristin Aoki
9bce0a34e3 fix: remove deprecated feedback link (#493) 2024-07-10 15:58:19 -04:00
renovate[bot]
085069abb0 fix(deps): update dependency @edx/frontend-lib-content-components to v2.5.0 (#1143)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-07-10 19:04:39 +00:00
Kyr
a22a260e27 feat: remove offset when stuio header exists (#491)
Co-authored-by: Kyrylo Hudym-Levkovych <kyr.hudym@kyrs-MacBook-Pro.local>
2024-07-10 14:35:25 -04:00
Kristin Aoki
b6ff6230e7 fix: text editor opening blank with no images (#492)
* fix: pasting and images only insert at beginning

* fix: add image click not showing gallery

* chore: increase code coverage

* fix: empty string when no srcs need updates

* fix: assest to static in raw editor
2024-07-10 13:53:40 -04:00
Braden MacDonald
ab9d57345b chore: remove unused fontawesome dependencies (#1149) 2024-07-10 14:05:02 +00:00
Rômulo Penido
71fcf9f168 fix: only show course blocks in the search modal (#1148) 2024-07-10 05:29:55 -07:00
Rômulo Penido
f60ddb579e feat: library home page ("bare bones") (#1076) 2024-07-10 05:20:00 -07:00
Braden MacDonald
117b4f10e7 chore: remove core-js and regenerator-runtime (#1032) 2024-07-10 05:07:29 -07:00
edX requirements bot
09822c2937 chore: update browserslist DB (#443) 2024-07-10 01:24:44 -07:00
Hunia Fatima
01d4b85205 perf: lockfile version check workflow file updated (#1107)
Co-authored-by: Hunia Fatima <hunia.fatima@A006-01315.local>
2024-07-10 11:05:43 +05:00
Yusuf Musleh
83489b0983 feat: Add filters/sorting for the libraries v2 tab on studio home (#1117) 2024-07-08 14:35:43 +00:00
Braden MacDonald
8cf26e1a75 Version bump for Paragon to 22.6.1, with stricter typing (#1146)
* fix(deps): update dependency @openedx/paragon to v22.6.1

* fix: lint errors from stricter types in new paragon version

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-07-08 10:10:08 -04:00
renovate[bot]
9528bfde62 chore(deps): update dependency meilisearch to ^0.41.0 (#1136) 2024-07-08 06:49:44 -07:00
Raymond Zhou
292663a5e3 Revert "fix(deps): update dependency @edx/frontend-lib-content-components to …" (#1142)
This reverts commit cdc9af2ed4.
2024-07-02 12:19:19 -04:00
renovate[bot]
9f0be768aa fix(deps): update dependency @edx/frontend-component-footer to v14.0.5 (#1121)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-07-01 17:11:05 +00:00
Kristin Aoki
efd73f9c0b fix: progress bar display for uploads (#1135) 2024-07-01 12:52:08 -04:00
renovate[bot]
cdc9af2ed4 fix(deps): update dependency @edx/frontend-lib-content-components to v2.4.3 (#1140)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-07-01 16:51:39 +00:00
Kristin Aoki
fc3cd9a9ce fix: image and paste insert (#490)
* fix: pasting and images only insert at beginning

* fix: add image click not showing gallery

* chore: increase code coverage
2024-07-01 12:29:05 -04:00
Kristin Aoki
eb3e6faba4 fix: update mapToStateProps to match changes in TinyMceWidget (#1133)
* fix: update mapToStateProps to match changes in TinyMceWidget

* feat: bump frontend-lib-content-components
2024-06-27 14:44:12 -04:00
Kristin Aoki
267823414e fix: simple editor without solution not loading (#489)
* fix: update initialize to only call required functions

* feat: update asset urls without asset object

* feat: add pagination to select image modal

* fix: lint errors

* chore: update tests

* fix: asset pattern regex match

* feat: update pagination to be button to prevent page skipping

* fix: e.target.error for feedback fields

* fix: failing snapshots

* fix: new simple problem load error
2024-06-27 12:54:59 -04:00
Braden MacDonald
a4859d2686 chore: convert all 'search-modal' code to TypeScript (#1129)
* chore: convert all 'search-modal' code to TypeScript

* fix: lint should check .ts[x] files too

* fix: remove unused dependency meilisearch-instantsearch
2024-06-27 12:54:01 -04:00
Kristin Aoki
22ea32cf01 feat: video upload progress modal (#1131)
* feat: add upload progress modal

* fix: increase code coverage

* fix: fix code to be more readable

* fix: delete empty file

* fix: failing test and lint

* fix: progress bar not updating

* feat: add missing abort controller on POST to edxVal
2024-06-26 18:00:07 -04:00
renovate[bot]
8b759bc867 fix(deps): update dependency @edx/frontend-lib-content-components to v2.4.1 (#1132)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-06-26 13:54:56 -04:00
bszabo
0e913739d4 Merge pull request #488 from openedx/revert-484-KristinAoki/improve-asset-loading
Revert "feat: improve asset loading"
2024-06-26 10:08:55 -04:00
bszabo
ba8141ea6a Revert "feat: improve asset loading (#484)"
This reverts commit f3ae225d64.
2024-06-26 10:02:59 -04:00
Kristin Aoki
9317b87564 Revert "feat: add upload progress modal (#1113)" (#1128)
This reverts commit 8ef804bd58.
2024-06-24 12:19:58 -04:00
Kristin Aoki
8ef804bd58 feat: add upload progress modal (#1113)
* feat: add upload progress modal

* fix: increase code coverage

* fix: fix code to be more readable

* fix: delete empty file
2024-06-24 10:53:49 -04:00
renovate[bot]
641419656f fix(deps): update dependency @edx/frontend-lib-content-components to v2.4.0 (#1118)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-06-24 09:02:05 -04:00
Jillian
6b6d3aaa7a Upgrade frontend-build to v14 (#1052)
* fix: warnings about Duplicate message id
* fix: paragon's Hyperlink no longer accepts a 'content' attribute
* test: ensure all act() calls are async
* test: Removed "async" from "describe"
* fix: DiscussionsSettings tests
* Don't nest userAction.click in act() -- nested act() statements have
  indeterminent behaviour.
* Use getBy* instead of findBy* with userAction to avoid nested act() statements
* Always await userEvent.click
* Use fireEvent.click when the onClick handlers need to be called
* Use queryBy* instead of getBy* when using .toBeInTheDocument or 
* fix: typo in data-testid
* test: Use useLocation to test route changes
* Don't nest userAction.click in act() -- nested act() statements have
* chore: fix lint:fix and lint errors
* remove "indent" setting from .eslintrc.js
* add @typescript-eslint/ prefix to eslint-disable-line statements where flagged by linter
* changed stylelint setting import-notation to "string"
* test: fix failing tests after upgrade
* fix: css error "target selector was not found"
* chore: upgrades dependency frontend-lib-content-components@2.3.0
* chore: bumps @edx/frontend-component-ai-translations to ^2.1.0

---------

Co-authored-by: Yusuf Musleh <yusuf@opencraft.com>
2024-06-22 00:14:46 +05:30
Braden MacDonald
28c7b32bd5 fix: don't accidentally bundle paragon CSS x2 2024-06-20 15:23:08 -07:00
Marcos Rigoli
3936737b48 feat: Include org filter when requesting LTI providers (#1114)
* feat: Include org filter when requesting LTI providers

* chore: Created silent version for CI testing to avoid log flooding
2024-06-20 11:17:08 -03:00
Yusuf Musleh
088a01d716 feat: Add lib v2/legacy tabs in studio home (#1050)
This PR adds a new configuration flag that shows/hides tabs in studio home along with some new functionality around to V1 and V2 Libraries.

When the new LIBRARY_MODE flag is set to "mixed" (default in dev) it will show "Libraries" and "Legacy Libraries" tabs that correspond to v1 and v2 tabs respectively.

When the new LIBRARY_MODE flag is set to "v1 only" (default in production) or "v2 only", only one tab "Libraries" is shown and only the respective libraries are fetched when the tab is clicked.

In addition to the above changes, the URL/route now updates when clicking on the tabs, and navigating to it directly would open up that tab as well as a new placeholder page that you will be redirected to when clicking on a v2 library if the library authoring MFE is not enabled.
2024-06-20 17:30:57 +05:30
vladislavkeblysh
c84e3229f6 feat: Validation for Start time and Stop time fields (#419)
* feat: fixed fields onblur

* feat: fixed fields onblur

* feat: added new tests
2024-06-18 16:51:32 -04:00
Dmytro
d2ddc9099f fix: Not used Number of attempts field (#473)
Co-authored-by: Dima Alipov <dimaalipov@MacBook-Pro-Dima.local>
2024-06-18 16:44:51 -04:00
Ihor Romaniuk
6ac0a6e562 fix: form group controls alignment (#423) 2024-06-18 16:40:49 -04:00
Navin Karkera
e2ed3bc7a7 refactor: show generic message on studio server error (#1112) 2024-06-18 16:07:46 -04:00
Adolfo R. Brandes
6a58779ffe Merge pull request #485 from arbrandes/update-codecov
build: Update codecov and use token
2024-06-17 11:57:12 -03:00
Kristin Aoki
f3ae225d64 feat: improve asset loading (#484)
* fix: update initialize to only call required functions

* feat: update asset urls without asset object

* feat: add pagination to select image modal

* fix: lint errors

* chore: update tests

* fix: asset pattern regex match

* feat: update pagination to be button to prevent page skipping

* fix: e.target.error for feedback fields

* fix: failing snapshots
2024-06-17 09:52:49 -04:00
Adolfo R. Brandes
74776b3663 build: Update codecov and use token
Update codecov to the latest version and start using the org-wide token for uploads.

See https://github.com/openedx/wg-frontend/issues/179
2024-06-14 11:44:01 -03:00
Jesper Hodge
db1250ee95 Revert "feat: flcc to 2.2.0 (#1106)" (#1111)
This reverts commit e22cce9fa6.
Reverts #1106

The reason is that the pipeline to deploy to stage broke. This commit is probably the reason.

The revert is temporary until the pipeline problem is solved.
2024-06-14 14:17:41 +00:00
Jillian
f20e5311a9 Fix some test warnings (#1062)
* fix: paragon's Hyperlink no longer accepts a 'content' attribute
* test: ensure all act() calls are async
* test: Removed "async" from "describe"
Returning a Promise from "describe" is not supported.
* fix: DiscussionsSettings tests
Previous commit revealed several issues with these tests
* Don't nest userAction.click in act() -- nested act() statements have indeterminent behaviour.
* Use getBy* instead of findBy* with userAction to avoid nested act() statements
* Use fireEvent.click when the onClick handlers need to be called
* Use queryBy* instead of getBy* when using .toBeInTheDocument or waitForElementToBeRemoved
  queryBy* return null when the element is not found.
* fix: typo in data-testid
Warning: React does not recognize the `data-testId` prop on a DOM
element. If you intentionally want it to appear in the DOM as a custom
attribute, spell it as lowercase `data-testid` instead.
* test: Use useLocation to test route changes
---------

Co-authored-by: Yusuf Musleh <yusuf@opencraft.com>
2024-06-13 10:37:26 +05:30
Raymond Zhou
e22cce9fa6 feat: flcc to 2.2.0 (#1106) 2024-06-12 12:42:00 -04:00
Bilal Qamar
252ad6a6b9 feat: updated frontend-build & frontend-platform major versions (#475) 2024-06-12 05:29:17 -04:00
Jillian
6760b75774 fix: warnings about Duplicate message id (#1061)
Fixes warnings about "duplicate message IDs", which seem to have been made by copy-paste errors.
2024-06-11 18:01:25 +05:30
Raymond Zhou
7f5e82a844 fix: handle null displayname (#1074) 2024-06-07 13:40:59 -04:00
Kristin Aoki
7aa2baaa8a fix: bump frontend-lib-content-components package (#1071) 2024-06-05 14:19:53 -04:00
Kristin Aoki
e543ccc2e1 fix: parser not saving unlimited attempts (#483)
* fix: default settings not loading for new problems

* fix: unlimited attempts not saving
2024-06-04 16:41:58 -04:00
Yusuf Musleh
8cde43eb5b fix: Search result redirect to unit lib component (#1027)
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-03 17:55:37 +05:30
Chris Chávez
460de7014e [FC-0042] Fix: Bug: Unusable "Languages" taxonomy appears in tagging drawer (#1031)
Hide language taxonomy when is empty
New message on search result when taxonomy is empty
Empty taxonomies message added in drawer
2024-06-03 17:36:54 +05:30
Kristin Aoki
9b4eb10342 fix: allow grace period minutes only (#1064)
* fix: allow grace period minutes only

* fix: zero minutes error
2024-05-31 15:30:00 -04:00
Kristin Aoki
a340320e8f fix: upgrade frontend -lib-content-componets package (#1060) 2024-05-31 09:56:03 -04:00
Kristin Aoki
a959c0543c fix: removal of content after problem type tags (#479)
* fix: removal of content after problem type tags

* fix: readability and error handling
2024-05-30 12:33:43 -04:00
Kristin Aoki
a585a13e97 fix: wrong lock status update message (#1053) 2024-05-28 11:52:39 -04:00
Kristin Aoki
435af2c36f fix: update date using utc timezone instead of local (#1043)
* fix: update date using utc timezone instead of local

* fix: lint error
2024-05-24 13:07:10 -04:00
Rodrigo Martin
732b7ed86c feat(AU-2035): Add disclaimer to AppSettingsModalBase (#1024) 2024-05-24 12:13:42 -03:00
Yusuf Musleh
d0b3328f26 feat: Import new taxonomy dialog flow (#1017)
This PR updates the existing import tags wizard to also handle  importing new taxonomies.
2024-05-24 19:19:26 +05:30
Chris Chávez
c3df0b0692 feat: Show toast when exporting course tags (#995)
Show in  in-progress toast when exporting course tags
2024-05-24 15:54:59 +05:30
Kristin Aoki
7247cc2d71 feat: bump frontend-lib-content-components 2.1.9 (#1028) 2024-05-22 15:40:04 -04:00
Kristin Aoki
3f987f9958 feat: improve error messaging and empty updates (#1025)
* feat: improve error messaging and empty updates

* chore: improve code coverage

* fix: update error messages

* fix: message title for saving handouts
2024-05-22 14:28:53 -04:00
Kristin Aoki
6c743f858d fix: video selection gallery scroll (#477)
* fix: video selection gallery scroll

* fix: lint errors
2024-05-22 12:02:25 -04:00
Peter Kulko
3647bcbbf9 fix: fixed rerun link (#1023)
* fix: fixed rerun link

* refactor: code refactoring

* refactor: updated tests

* refactor: after review
2024-05-21 12:58:53 -04:00
bszabo
54003af07c fix: issue-1018 remove reference to edX user\nIn configure wiki opera… (#1021)
* fix: issue-1018 remove reference to edX user\nIn configure wiki operation

* fix: issue-1018 keep to original they wording
2024-05-21 11:24:21 -04:00
renovate[bot]
f34157e11c fix(deps): update dependency @edx/frontend-lib-content-components to v2.1.8 (#997)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-05-16 14:26:42 +00:00
renovate[bot]
8a491cc365 fix(deps): update dependency @openedx/paragon to v22.4.0 (#963)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-05-16 14:15:18 +00:00
Leangseu Kim
7d9dd7535b chore: lock @edx/react-unit-test-utils 2024-05-16 10:02:50 -04:00
Ihor Romaniuk
7c539f346b fix: info icon shrinking on advanced settings page (#984) 2024-05-15 13:02:05 -03:00
ABBOUD Moncef
3315205d15 fix: video status badge translation (#438) 2024-05-13 15:26:07 -04:00
Navin Karkera
d882f2f856 feat: discussion setting and release & due date setting (#976)
* fix: hide release and due dates config in self paced courses

* feat: discussion enable setting for unit in outline

* refactor: message text

Co-authored-by: Kristin Aoki <42981026+KristinAoki@users.noreply.github.com>

* fix: modal dialog overflow

---------

Co-authored-by: Kristin Aoki <42981026+KristinAoki@users.noreply.github.com>
2024-05-13 11:21:18 -04:00
Leangseu Kim
65132eead2 fix: allow page grid to take plugin slot id instead (#994)
* fix: allow page grid to take plugin slot id instead

* chore: add config check

* chore: linting

* chore: variable more readable
2024-05-13 10:39:11 -04:00
Raymond Zhou
e0b70f2b17 fix: HTML screen should expand with content (#474) 2024-04-18 14:48:10 -04:00
Jesper Hodge
dedcb14386 fix: trigger pipeline release for dependency fixes (#472) 2024-04-04 13:26:46 -04:00
dependabot[bot]
cf46e6c6c9 build(deps-dev): bump follow-redirects from 1.15.5 to 1.15.6 (#471)
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 13:04:17 -04:00
dependabot[bot]
77cdf2614e build(deps): bump express and @openedx/frontend-build (#470)
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 13:03:56 -04:00
dependabot[bot]
fd3c871585 build(deps): bump follow-redirects from 1.15.5 to 1.15.6 in /www (#469)
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 13:02:58 -04:00
dependabot[bot]
2bf04b8be6 build(deps-dev): bump webpack-dev-middleware from 5.3.3 to 5.3.4 (#468)
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:56:01 -04:00
dependabot[bot]
4ba62d7ee4 build(deps): bump webpack-dev-middleware from 5.3.3 to 5.3.4 in /www (#467)
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:52:44 -04:00
Kristin Aoki
905bea0d59 fix: default setting not updating with updated default settings (#466) 2024-03-28 15:00:16 -04:00
Kristin Aoki
4fa169556e fix: remove useEffect that fires before search params (#465)
* fix: remove useEffect that fires before search params

* fix: remove disable lint lines
2024-03-18 15:31:00 -04:00
Jeremy Ristau
85ca350591 Merge pull request #463 from openedx/gh-team-update
chore: Github team update
2024-03-12 09:34:04 -04:00
Jeremy Ristau
d8503fbfe2 chore: update CODEOWNERS 2024-03-11 22:33:21 -04:00
Jeremy Ristau
9a8deced9b chore: update CODEOWNERS 2024-03-11 22:32:32 -04:00
Jeremy Ristau
1797707a9a chore: update openedx.yaml 2024-03-11 22:32:01 -04:00
Jesper Hodge
7f73b895d1 fix: gallery (#462) 2024-03-11 15:43:35 -04:00
Jesper Hodge
679e15ed00 fix: gallery 2024-03-11 19:39:18 +00:00
Jesper Hodge
fb2a79985e Fix: selectable box in gallery (#461)
This fixes a bug where images and videos are not clickable when using the Gallery component.
Please see https://github.com/openedx/frontend-lib-content-components/pull/460 for a full description of the bug.
2024-03-11 15:26:54 -04:00
Jesper Hodge
b3b2881efb Revert "fix: gallery"
This reverts commit 736a786e12.
2024-03-11 18:42:17 +00:00
Jesper Hodge
736a786e12 fix: gallery 2024-03-11 18:41:17 +00:00
Jesper Hodge
be00028c4a Fix Problem Type Select (#460)
* fix: override selectablebox with copy of version from edx paragon 21.5.6

* chore: remove console logs

* fix: lint

* chore: update snapshots
2024-03-11 12:59:05 -04:00
Jesper Hodge
5069cf8638 fix: problem type not selectable (#459)
JIRA: https://2u-internal.atlassian.net/browse/TNL-11465

This is a problem we're experiencing on stage due to a bug in paragon. This is intended as a temporary fix until we can dig into why this is happening in paragon. But basically the Context for the SelectableBox is missing a provider in paragon. The short-term fix is to copy over paragon's selectablebox component and fix it. This is done so that our quick fix doesn't break anything else in paragon for now or cause any unexpected issues.
2024-03-07 15:21:02 -05:00
Jeremy Ristau
4e78c07dac Merge pull request #458 from openedx/catalog-info-updates
chore: update tnl team name
2024-03-05 09:19:33 -05:00
Jeremy Ristau
e5a469f7ea chore: update tnl team name 2024-03-04 21:46:23 -05:00
Jhon Vente
90d5ac4ffc feat: TinyMCE plugin insert iframe (#427) 2024-02-16 14:02:34 -05:00
Brian Smith
f33a3b5521 fix(deps)!: support paragon and frontend-build in openedx scope (#457)
BREAKING CHANGE: frontend-platform peer dependency updated to ^7.0.1
BREAKING CHANGE: @edx/paragon peer dependency updated to @openedx/paragon
2024-02-09 14:41:16 -05:00
Feanil Patel
62bff35fcd Merge pull request #381 from Mashal-m/mashal-m/update_lockfile
refactor: updated lock file version check to use new workflow
2024-02-08 14:30:16 -05:00
Syed Ali Abbas Zaidi
dcdaace08d feat: migrate enzyme to edx react-unit-test-utils 2024-01-24 16:51:53 -05:00
Kristin Aoki
1bc4e51c22 fix: url for video uploads (#453)
* fix: url for video uploads

* fix: possible undefined error in postUrlRedirect
2024-01-16 11:00:43 -05:00
Kristin Aoki
4653322fca fix: video upload api fetch body (#454) 2024-01-10 17:13:29 -05:00
Kristin Aoki
13f039ae4c feat: add blockId to return url (#442) 2024-01-05 09:30:35 -05:00
Jesper Hodge
8833f7bfca fix: library editor not working locally (#448)
Internal issue: https://2u-internal.atlassian.net/browse/TNL-11299

In library-authoring, content blocks could not be edited locally due to some problem with the Accept header. This caused some 404s. Visually you would get an infinite loading spinner.

This depends on openedx/frontend-app-library-authoring#399.
2024-01-04 14:36:07 -05:00
Jesper Hodge
70581c54ab refactor: abstract XML parser output operations (#445)
This is a refactoring for a part of the ReactStateXMLParser. I wanted to use functions that are more generic and not just handle a list of edge cases. So I encapsulated the operation that was done in this part of the code to a function `findNodesAndRemoveTheirParentNodes` which is more generic and could be used for different operations.
2024-01-02 15:04:03 -05:00
kenclary
69452344d8 Merge pull request #447 from openedx/kenclary/plugins
fix: update public plugins repo version
2023-12-21 11:13:10 -05:00
Ken Clary
8fdb395680 fix: update public plugins repo version 2023-12-21 11:09:29 -05:00
Jesper Hodge
e77d3d1014 fix: do comment change to trigger release (#446) 2023-12-20 17:23:46 -05:00
Jesper Hodge
50c580cca2 fix editor deleting description (#444)
internal issue: https://2u-internal.atlassian.net/browse/TNL-11311

This fixes a bug where the editor was deleting the OLX tag when editing in the simple editor and then saving.
Also the description was being converted to em for the simple editor, but then not converted back. However, the xblock renders label and then em in reverse order for some reason.
To fix it, the em gets converted back to description now, but not for every em tag (added a class "olx_description" for the tags that should be converted).
2023-12-20 16:44:27 -05:00
kenclary
47a3fd6836 Merge pull request #443 from openedx/kenclary/plugins
fix: re-enable private plugins, with newer version of public plugin repo
2023-12-19 13:09:13 -05:00
Ken Clary
82f6d7d3ca fix: re-enable private plugins, with newer version of public plugin repo 2023-12-19 12:28:57 -05:00
dependabot[bot]
613220441f build(deps): bump sharp from 0.32.1 to 0.32.6 in /www (#441)
Bumps [sharp](https://github.com/lovell/sharp) from 0.32.1 to 0.32.6.
- [Release notes](https://github.com/lovell/sharp/releases)
- [Changelog](https://github.com/lovell/sharp/blob/main/docs/changelog.md)
- [Commits](https://github.com/lovell/sharp/compare/v0.32.1...v0.32.6)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-12-18 09:56:26 -05:00
dependabot[bot]
57937995e2 build(deps): bump @babel/traverse from 7.21.5 to 7.23.6 in /www (#440)
Bumps [@babel/traverse](https://github.com/babel/babel/tree/HEAD/packages/babel-traverse) from 7.21.5 to 7.23.6.
- [Release notes](https://github.com/babel/babel/releases)
- [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md)
- [Commits](https://github.com/babel/babel/commits/v7.23.6/packages/babel-traverse)

---
updated-dependencies:
- dependency-name: "@babel/traverse"
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-12-18 09:37:57 -05:00
dependabot[bot]
60ccf0fb53 build(deps): bump @babel/traverse from 7.22.5 to 7.23.6 (#439)
Bumps [@babel/traverse](https://github.com/babel/babel/tree/HEAD/packages/babel-traverse) from 7.22.5 to 7.23.6.
- [Release notes](https://github.com/babel/babel/releases)
- [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md)
- [Commits](https://github.com/babel/babel/commits/v7.23.6/packages/babel-traverse)

---
updated-dependencies:
- dependency-name: "@babel/traverse"
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-12-18 09:28:08 -05:00
Jesper Hodge
287cc23ee7 fix: disable a11ychecker and powerpaste plugins (#437) 2023-12-14 15:09:04 -05:00
Jesper Hodge
56ffc495dd chore: disable tinymce plugins (#436) 2023-12-14 14:55:36 -05:00
Jesper Hodge
fbd3d8506f fix: editor container can scroll again (#435)
This is a fix to a problem where some css prevented the editors from scrolling.
2023-12-14 11:53:26 -05:00
kenclary
4483214de1 Merge pull request #434 from openedx/revert-432-kenclary/plugins
Revert "fix: disable paid plugins, to test prod issue in stage"
2023-12-13 10:46:40 -05:00
kenclary
aa17203e07 Revert "fix: disable paid plugins, to test prod issue in stage" 2023-12-13 10:39:37 -05:00
Jesper Hodge
57042c90bf Revert "fix: get library blocks editing to work locally (#431)" (#433)
This reverts commit fd3ed5d146.
2023-12-12 15:57:03 -05:00
kenclary
6dab3a1cea Merge pull request #432 from openedx/kenclary/plugins
fix: disable paid plugins, to test prod issue in stage
2023-12-12 15:30:44 -05:00
Ken Clary
7c7b36b402 fix: disable paid plugins, to test prod issue in stage 2023-12-12 15:22:15 -05:00
Jesper Hodge
fd3ed5d146 fix: get library blocks editing to work locally (#431)
* fix: get library blocks editing to work locally

* fix: tests
2023-12-12 14:34:27 -05:00
kenclary
1137c88a02 Merge pull request #428 from openedx/kenclary/plugins
fix: misssing part of paid plugin configuration
2023-12-11 12:07:25 -05:00
Ken Clary
b0ca07d801 fix: misssing part of paid plugin configuration 2023-12-11 11:46:57 -05:00
Artur Gaspar
1c9771b332 feat: video gallery thumbnail fallback (#412) 2023-12-04 09:58:40 -05:00
Artur Gaspar
1ddaf9a662 fix: video editor and uploader layout fixes (#410)
* fix: video upload page layout fixes

* fix: video editor thumbnail fallback icon colour and size

* fix: video uploader close button go back instead of closing app

Change the video uploader close button to always go back in navigation history,
and change the gallery to replace its location with the video uploader's when
automatically loading it due to an empty video list, so that when the uploader
goes back in the history, it goes to what was before the gallery.

* fix: video editor spinners vertical alignment

The Editor component uses the pgn__modal-fullscreen class to be fullscreen,
but it is not structured like a Paragon FullscreenModal and the fullscreen
positioning style is not applied correctly, particularly in the case where
the content is smaller than the body - a vertically centred component will
be centred to the content size, not to the screen.

Here we directly apply the style that would have applied to the relevant
elements of a Paragon FullscreenModal.

* fix: use trailingElement for video uploader input button

Also styles the button so it looks the same on hover/active.
2023-12-04 09:58:17 -05:00
Artur Gaspar
2aeb094315 fix: video gallery design fixes (#407) 2023-12-04 09:35:30 -05:00
Dmytro
ed051c3543 fix: default display of Show Answer and Show reset option (#403)
Hardcoding values for showAnswer and showResetButton
in initialState leads to incorrect selection of additional
states specified in Advanced settings.
2023-12-01 09:26:12 -05:00
Dmytro
60439d5be6 fix: displaying the correct default randomization value (#413) 2023-11-30 16:33:25 -05:00
ABBOUD Moncef
8928d35f17 fix: make filter dropdown closable by clicking outside + revert to single-value video filter (#414) 2023-11-30 14:43:12 -05:00
Ihor Romaniuk
bad763596c fix: video thumbnail availability (#421) 2023-11-30 14:34:19 -05:00
kenclary
c0bc54a664 Merge pull request #420 from openedx/kenclary/plugins
feat: use plugins repo, and enable accessibility checker and powerpaste
2023-11-22 10:50:56 -05:00
Ken Clary
5342f164a2 feat: use plugins repo, and enable accessibility checker and powerpaste 2023-11-21 14:27:10 -05:00
Jeremy Ristau
fc8070025b Merge pull request #415 from Mashal-m/mashal-m/update-README
refactor: updated README file to reflect template changes
2023-10-31 08:39:15 -04:00
Kristin Aoki
3aa9088309 fix: upgrade paragon (#416) 2023-10-30 11:39:09 -04:00
Syed Ali Abbas Zaidi
42614b8d8e chore: bump frontend-platform (#409) 2023-10-27 09:30:51 -04:00
mashal-m
b3811b8f4d refactor: updated README file to reflect template changes 2023-10-27 12:07:46 +05:00
Jesper Hodge
cfa4577b75 chore: add test change for flcc automerge (#408) 2023-10-11 13:58:42 -04:00
kenclary
2377eadcc0 Merge pull request #400 from abdullahwaheed/abdullahwaheed/react-intl-to-formatjs
feat: babel-plugin-react-intl to babel-plugin-formatjs migration
2023-10-11 11:38:30 -04:00
kenclary
b55b86cf56 Merge pull request #404 from open-craft/navin/fix-max-attempts-setting
fix: make max attempts setting fallback to default
2023-10-11 10:04:16 -04:00
Abdullah Waheed
71fd5fb1e0 fix: upgraded frontend-build to fix security issue 2023-10-11 18:44:01 +05:00
Abdullah Waheed
9ddcba2763 Merge branch 'main' of github.com:openedx/frontend-lib-content-components into abdullahwaheed/react-intl-to-formatjs 2023-10-11 18:43:34 +05:00
Jeremy Ristau
a76c93c789 Merge pull request #361 from ghassanmas/fix-360
fix: resolve  #360 make style consistent across outlines
2023-10-10 14:28:38 -04:00
Navin Karkera
6889cd1e82 test: add test for initial attempts display value 2023-10-10 20:19:00 +05:30
Navin Karkera
35a2f3bb7f fix: use only null in state for empty value 2023-10-10 19:52:39 +05:30
Navin Karkera
e676616386 fix: snapshots 2023-10-10 12:53:11 +05:30
Navin Karkera
2209e5b963 fix: lint issues 2023-10-10 12:19:14 +05:30
Navin Karkera
82b770bdef refactor: improve hooks condition handling 2023-10-10 12:13:46 +05:30
Navin Karkera
398839d76c refactor: improve setting parser condition handling 2023-10-09 21:29:53 +05:30
Navin Karkera
5df26bf83b test: fix related test cases 2023-10-09 20:34:05 +05:30
Navin Karkera
c70679da54 fix: make max attempts setting fallback to default
The max attempts setting for a problem xblock should be set to null for
course default max attempt setting to take effect. This makes sure that
xblock setting is updated if course default is updated.
2023-10-06 20:35:53 +05:30
Abdullah Waheed
e474a6fc91 feat: babel-plugin-react-intl to babel-plugin-formatjs migration 2023-10-04 22:17:12 +05:00
Kristin Aoki
564d724d5b feat: remove footer component (#397) 2023-10-02 10:29:37 -04:00
Kristin Aoki
a0089eb1be fix: typeahead sort and styling (#396) 2023-09-27 12:09:27 -04:00
kenclary
eb320abfed Merge pull request #386 from open-craft/navin/partial-credit
fix: switch to advanced editor for partial credit support
2023-09-20 11:58:52 -04:00
Kristin Aoki
773812c3e1 fix: block url conditional (#393) 2023-09-15 11:23:01 -04:00
Kristin Aoki
9eefc07832 fix: empty v2 library studio view (#394) 2023-09-15 09:05:51 -04:00
Kristin Aoki
bc25f9c21b fix: v2 libraries default open to advanced editor (#392) 2023-09-14 11:41:14 -04:00
kenclary
4cf99ab930 Merge pull request #388 from open-craft/farhaan/bb-7835-fix-styling
fix: Fix styling for components in dropzone
2023-09-14 11:37:47 -04:00
Farhaan Bukhsh
db8929d1a8 fix: Fix styling for components in dropzone
Signed-off-by: Farhaan Bukhsh <farhaan@opencraft.com>
2023-09-14 12:52:26 +05:30
Kristin Aoki
126e662d80 feat: remove replace video button for libs (#390) 2023-09-13 13:35:42 -04:00
Kristin Aoki
39aa5aa749 feat: update isLibrary check to include v2 libs (#389) 2023-09-13 13:13:18 -04:00
Kristin Aoki
73af4317f6 fix: name of blank problems (#391) 2023-09-13 10:55:28 -04:00
Kristin Aoki
4f76b7c85e fix: sort items in typeahead dropdown (#387) 2023-09-12 09:48:43 -04:00
Syed Ali Abbas Zaidi
c44c72cec0 feat: upgrade react router to v6 (#280) 2023-09-12 09:04:42 -04:00
Navin Karkera
e3f5bbfe0c fix: switch to advanced editor for partial credit support
This commit reverts to advanced editor when partial_credit attribute is
added to multichoice, single select and numerical problems. Without this
change, the partial_credit attribute is removed from the problem on the
next edit.
2023-09-11 20:14:04 +02:00
Mohamed Akram
25e4e39953 fix: preformatted content being re-formatted (#376)
The issue had to do with how Firefox handles pasting newlines inside a <pre
contenteditable> tag (TinyMCE's editor works via contenteditable) and
fast-xml-parser's parsing. In Firefox, newlines are converted to <br> when
pasted, while Chrome preserves them. The parser by default trims spaces in text
nodes. In Firefox, the parser creates individual text nodes between the <br>
elements, and those have leading spaces in the example. In Chrome, there are no
<br> elements and the entire content is a single text node as-is. Setting
trimValues to false disables the trimming and resolves this issue in Firefox.

While investigating this, I noticed the builder also mishandles <br /> tags
emitted by the editor, converting them to <br></br>. The unpairedTags option in
the builder ensures they are output correctly as a single tag, and setting
suppressUnpairedNode to false ensures that single tag is <br/> rather than <br>
to remain XML compatible.

While trying to resolve this, I was looking into the paste plugin in TinyMCE.
It changes the behavior of pasting, making it more consistent between Chrome
and Firefox (i.e. both emit <br>) and is incorporated into TinyMCE 6 core.
Unfortunately, it seems to mangle pasting inside a <pre> tag by inserting
redundant nbsp characters (tinymce/tinymce#9017). TinyMCE 6 also outputs <br>
rather than <br /> - adding unpairedTags to the parser options is meant to
handle this, but it does not seem to behave entirely correct
(NaturalIntelligence/fast-xml-parser#609). These should be kept in mind if/when
upgrading to TinyMCE 6 (the different behaviors can be seen easily at
https://fiddle.tiny.cloud).
2023-09-11 10:47:04 -04:00
Kristin Aoki
9ebe187029 fix: undefined set selection for image modal (#384) 2023-09-06 16:48:21 -04:00
Jesper Hodge
8fe8bc1587 docs: document jest troubleshooting (#382) 2023-08-28 12:05:47 -04:00
mashal-m
e23a0887ce refactor: update lock file version 2023-08-28 13:54:54 +05:00
kenclary
8e659527f0 Merge pull request #366 from open-craft/farhaan/fix-drag-drop-component
Re-write the dropzone component and fix styling issues
2023-08-25 12:50:24 -04:00
kenclary
45e4bc5376 Merge pull request #368 from open-craft/kshitij/fix-video-sort-filter
fix: Video Gallery filters and sorting
2023-08-23 06:43:56 -07:00
kenclary
259b9f3d1f fix: don't get returnUrl for v2 blocks. (#380) 2023-08-21 19:57:08 -04:00
Jesper Hodge
e691df9cb5 fix: answer text flipped (#379)
This fixes a bug where an answer text was flipped in terms of the character order when typing (TNL-10980). One of the prop names of the TinyMceWidget that is imported in course-authoring had to be changed, so this goes together with https://github.com/openedx/frontend-app-course-authoring/pull/575.
2023-08-21 16:43:46 -04:00
Farhaan Bukhsh
b7a04e17da fix: Fixing the accessing of undefined variables in video
Signed-off-by: Farhaan Bukhsh <farhaan@opencraft.com>
2023-08-21 10:11:56 +05:30
Farhaan Bukhsh
f822d95d6a fix: Used Dropzone instead of having custom component
This PR fixes style component and remove any new component introduced.
We introduce a new thumbnail for setting page as well.

Signed-off-by: Farhaan Bukhsh <farhaan@opencraft.com>
2023-08-21 10:11:56 +05:30
kenclary
12266836eb Merge pull request #378 from openedx/kenclary/supplemental
fix: revert to blank-url version of error handling, for now
2023-08-18 14:09:29 -07:00
Ken Clary
3c1f870aac fix: revert to blank-url version of error handling, for now 2023-08-18 13:46:57 -07:00
kenclary
6de926ce7e Merge pull request #377 from openedx/kenclary/supplimental
fix: don't even bother fetching block ancestor in v2.
2023-08-17 17:37:31 -04:00
Ken Clary
9079196309 fix: don't even bother fetching block ancestor in v2. 2023-08-17 14:23:13 -07:00
Kshitij Sobti
fb7caffdd5 feat: Allow selecting my multiple filters in video gallery
The sort and filter UI of the video gallery was not working, this fixes that
issue, and also adds a new UI for filering videos that allows filtering videos
to include more than one status.

It also fixes the hooks related to VideoGallery to avoid potential bugs in the
future and updates tests to use react testing library instead of enzyme.

It also reduces the padding in gallery page.
2023-08-17 13:21:47 +05:30
Kristin Aoki
9438a5b89a feat: add typeahead component (#375) 2023-08-16 18:18:06 -04:00
Kristin Aoki
e9c0f6cc82 fix: selectedVideo undefined error (#374) 2023-08-11 14:05:07 -04:00
Raymond Zhou
a18c45f0db feat: call failRequest on fetch failure (#373) 2023-08-10 12:58:14 -04:00
Kristin Aoki
669fbfb3d2 fix: blockId checks for v1 or v2 libraries (#372) 2023-08-09 14:55:46 -04:00
Kristin Aoki
918370f743 fix: error return while editor loading (#369) 2023-08-08 12:41:15 -04:00
ruzniaievdm
d25ae09273 feat: Add TinyMceWidget on export (#365) 2023-08-03 12:44:07 -04:00
kenclary
38b85f70ac Merge pull request #343 from Mashal-m/mashal-m/react-upgrade-to-v17
feat: update react & react-dom to v17
2023-07-31 14:54:39 -04:00
mashal-m
cc4a7cc83d build: update lock file 2023-07-31 16:52:05 +05:00
mashal-m
3f98349f94 build: update lock file 2023-07-31 16:47:50 +05:00
mashal-m
ed7e98b6ea Merge branch 'main' of https://github.com/openedx/frontend-lib-content-components into mashal-m/react-upgrade-to-v17 2023-07-31 16:23:46 +05:00
kenclary
a935d296c9 Merge pull request #355 from openedx/kenclary/TNL-10743
feat: more/correct v2 url handling. TNL-10742
2023-07-20 15:31:33 -04:00
Jesper Hodge
3565741839 fix image resize (#299)
Description:
This is a bug where the image resizing in text editor and problem editor was completely broken. Putting in a text value when the aspect ratio lock was enabled would change both values but not to the size you wanted. If you disabled the lock, not just one but both values would change.

This is a problem that mostly affects images that are rectangular, not square. There's an example image below which is one that caused problems on prod.

Main fixes:
when I keep the image ratio locked, I can change one value (like width) and the other will jump to the proportionate value, but rounded to full pixels.
when I unlock the aspect ratio and change a value, then click save on the image dimension modal, only the one value will change, which will stretch the image in whatever direction. This is reflected in the tinymce image and then the updated value will appear when I reopen the image dimension modal. It is not possible to reset the image to the original dimensions any longer. The new values are saved.
The image dimensions in the edit image settings modal should always reflect the actual dimensions of the image when I look at it e.g. in the course outline. (Otherwise I may click save and the image is squished.)
There was a problem with deselecting an image: when you edit image dimensions and then save or press cancel, the "edit image" button will not disappear, but the image is not selected anymore. When you do not click anything else but immediately click on this button, sometimes (at least the second or third time you do this) this will throw an error. I fixed it so it will just open the default "select image" modal.
Other requirements:
Resizing the image means that when I open the dimensions update, I see the new dimensions.
Images in the editor are now displayed with the correct dimensions, proportional or stretched, if those dimensions don't exceed the size of the editor.
A known smaller bug emerging from this is that when you have more than one instance of the same image in the same editor, you cannot get or set its dimensions correctly. I believe I have gotten it into the following state: When you click one of the copies, you will either get the correct dimensions of the selected copy, or if not, it will display the original image dimensions. When you edit the dimensions, the correct copy of the image will be updated.
Out of Scope:
This cannot handle more than one instance of the same image properly. There will be a separate bug issue for this.
Sometimes, when you edit image dimensions and then reopen the image dimension modal, the dimensions will be null and thus just not appear in the modal - randomly. This is a bug as well.
2023-07-20 14:52:21 -04:00
Ken Clary
7178e5e4c9 feat: more/correct v2 url handling. TNL-10742 2023-07-20 14:37:22 -04:00
Raymond Zhou
4a5eaaf15e feat: setup game editor work (#363) 2023-07-20 04:59:54 -04:00
Kristin Aoki
7939af4737 fix: partner portal typo (#362) 2023-07-11 14:10:04 -04:00
Kristin Aoki
3586307ee7 feat: react based studio footer (#359) 2023-07-11 10:30:52 -04:00
Ghassan Maslamani
2bc447fab0 fix: resolve #360 make style consistent across outlines
This changes make the style consistent across the first three
 headline line levels

 1. The first level add CSS prop initial
 2. The second level change prop from capital to inital
 3. No change
2023-07-11 14:50:29 +03:00
Jesper Hodge
f942ef9594 fix enable rules of hooks (#329)
This is a PR enabling eslint "rules-of-hooks".

This lint rule catches some very annoying bugs and enforces you to use correct naming for custom hooks (prepend with "use"), which is a mandatory react rule and important for a number of reasons.

I added eslint-disable statements wherever the rules are broken, and if this is merged, I would expect new code not to break the rules any longer.

I strongly suggest that the much better way to extract and reuse hooks and logic from components is the way the community does it and the React docs suggest. The new React docs are really fantastic in this regard and you can use the patterns found there very well to have an excellent application. https://react.dev/learn/reusing-logic-with-custom-hooks
2023-07-05 15:11:07 -04:00
mashal-m
505704e8f3 build: update edx pkgs and peer Dep 2023-07-05 16:44:24 +05:00
kenclary
c09faa3b09 Merge pull request #354 from open-craft/farhaan/bb-7522-fix-spinner-for-video
feat: Add loading spinner to the video upload page
2023-06-30 11:10:50 -04:00
kenclary
2ab7aa5cea Merge pull request #350 from open-craft/navin/hide-hide-option
refactor: hide switch to hide selected videos
2023-06-30 11:10:42 -04:00
Raymond Zhou
436fdfc470 feat: parse out explanation text (#358) 2023-06-28 14:05:31 -04:00
Raymond Zhou
86b67022ba feat: parser changes (#356) 2023-06-27 12:50:07 -04:00
Navin Karkera
7a53de4f2d refactor: hide switch to hide selected videos
The backend for hiding selected videos is not implemented and it is currently not required, so this commit hides the option.
2023-06-24 19:39:38 +05:30
Farhaan Bukhsh
ab640fb561 refactor: Moved the function to hooks in order to keep the components
dumb

It is done to make sure the business logic is not in a component and can
be individually tested.

Signed-off-by: Farhaan Bukhsh <farhaan@opencraft.com>
2023-06-23 20:01:33 +05:30
Farhaan Bukhsh
4d684d620e fix: Added tests for video upload
Signed-off-by: Farhaan Bukhsh <farhaan@opencraft.com>
2023-06-23 20:01:33 +05:30
Kristin Aoki
714c946f0f fix: persisting title from previous editor load (#353) 2023-06-22 19:22:40 -04:00
kenclary
769cdb08fb Merge pull request #344 from jansenk/events
feat: send event on checkbox change
2023-06-22 10:37:20 -04:00
jansenk
1a0cc2db2a style: rename function 2023-06-22 09:26:11 -04:00
Farhaan Bukhsh
a86b844208 feat: Add spinner to video element to load
Signed-off-by: Farhaan Bukhsh <farhaan@opencraft.com>
2023-06-22 13:46:08 +05:30
Kristin Aoki
c8e85fae0b fix: failure toast and close modal bugs (#352) 2023-06-21 13:48:54 -04:00
mashal-m
42210e7c89 build: update lock file 2023-06-21 14:16:10 +05:00
mashal-m
ef662f9ceb Merge branch 'main' of https://github.com/openedx/frontend-lib-content-components into mashal-m/react-upgrade-to-v17 2023-06-21 14:10:01 +05:00
Mashal Malik
8a2c725eda chore: update lock version file (#351) 2023-06-21 05:03:50 -04:00
mashal-m
530183a297 build: update paragon version 2023-06-19 15:55:53 +05:00
Kristin Aoki
b0fef766eb feat: add ErrorAlert to exports (#349) 2023-06-15 15:28:12 -04:00
Raymond Zhou
83f034e500 feat: save spacing for richText parser (#348) 2023-06-14 12:04:44 -04:00
kenclary
ac2444c258 Merge pull request #347 from openedx/kenclary/TNL-10742
feat: v2 xblocks should use v2 url. TNL-10742
2023-06-13 16:32:44 -04:00
Kristin Aoki
665a53a713 feat: add draggable list (#342) 2023-06-13 16:31:17 -04:00
Kristin Aoki
8eb10b7b12 feat: update returnToUnit to include api response (#346) 2023-06-13 15:29:04 -04:00
Ken Clary
ffd311881a feat: v2 xblocks should use v2 url. TNL-10742 2023-06-13 15:09:04 -04:00
jansenk
491d870cd2 style: quality 2023-06-12 10:25:04 -04:00
jansenk
e2535b2467 feat: send event on checkbox change 2023-06-12 09:56:32 -04:00
jansenk
0111d1c2f5 fix: remove param from end of video features url 2023-06-12 09:56:07 -04:00
mashal-m
86b0c4da82 feat: update react & react-dom to v17 2023-06-12 18:13:45 +05:00
Kristin Aoki
82b0f67f11 feat: add returnUrl prop to editors (#341) 2023-06-08 13:32:53 -04:00
Kristin Aoki
f3c4669604 fix: doc typos (#340) 2023-06-06 11:00:37 -04:00
kenclary
43b6ec7708 Merge pull request #339 from openedx/revert-338-kenclary/update-react
Revert "chore: update react to 17, etc. TNL-10715"
2023-06-05 16:43:00 -04:00
kenclary
6317c46120 Revert "chore: update react to 17, etc. TNL-10715" 2023-06-05 16:38:58 -04:00
Kristin Aoki
831c6096cb fix: choice parsing for answers with multi lines (#337) 2023-06-02 11:13:33 -04:00
kenclary
dd79d49533 Merge pull request #338 from openedx/kenclary/update-react
chore: update react to 17, etc. TNL-10715
2023-06-02 09:17:51 -04:00
Ken Clary
648e700818 chore: update react to 17, etc. TNL-10715 2023-06-01 16:11:58 -04:00
Kristin Aoki
88629d6df1 fix: olx parsers with formatted text (#336) 2023-05-30 11:42:59 -04:00
Bilal Qamar
2199a24dd7 feat: upgraded to node v18, added .nvmrc and updated workflows (#281)
Co-authored-by: Abdullah Waheed <abdullah.waheed@arbisoft.com>
2023-05-26 09:52:56 -04:00
Kristin Aoki
d80b6faaad feat: replace canvas with jest-canvas-mock (#335) 2023-05-24 09:56:39 -04:00
Kristin Aoki
e0c0c918d0 fix: feedback clearing on type change (#334) 2023-05-22 09:57:22 -04:00
kenclary
9239d2b8b1 Merge pull request #333 from open-craft/chris/video-editor-flow-fix
fix: Temporal testing line deleted from VideoUploadEditor::hooks.js
2023-05-18 12:42:39 -04:00
XnpioChV
3bdafd6e36 fix: Temporal testing line deleted from VideoUploadEditor::hooks.js 2023-05-18 11:31:35 -05:00
kenclary
aee971924f Merge pull request #326 from open-craft/chris/FAL-3383-new-video-editor-flow
[FAL-3383] Implement new video UX flow on new video editor
2023-05-18 11:49:10 -04:00
Raymond Zhou
741b83bdf2 fix: delete answers without changing expandable (#328) 2023-05-17 14:22:51 -04:00
Kristin Aoki
be8f9ecc86 feat: add raw olx settings parser (#332) 2023-05-15 15:12:32 -04:00
Mashal Malik
084d61ffa1 Update codecov and add browserslist config (#209)
* refactor: update code cov and add browserlist config
2023-05-11 14:44:48 -04:00
Kristin Aoki
99cd3bf1d9 chore: refactor OLXParser and related doc strings (#330) 2023-05-11 13:30:47 -04:00
Jesper Hodge
8cd222b9fa Add ADR for organizing our folder structure in a feature-based manner (#296)
* docs: add feature based application organization adr

* Update 0006-feature-based-application-organization.rst

* docs: update adr
2023-05-11 12:06:11 -04:00
Kristin Aoki
1d66a9d14d feat: remove matlab api widget and refs (#331) 2023-05-10 15:17:16 -04:00
Kristin Aoki
109334a9bf fix: show setting note for course set social share (#327) 2023-05-09 09:27:32 -04:00
Jesper Hodge
003f33152d docs: add adr with test naming conventions (#320)
* docs: add adr with test naming conventions

* docs: fix rst

* docs: add further reading section

* Update docs/decisions/0006-test-names.rst
2023-05-05 16:20:19 -04:00
XnpioChV
5c432a03db test: Adding missing test to reach coverage/patch 2023-05-05 14:10:18 -05:00
XnpioChV
15393f3da1 style: Fixing styles on gallery, editor and uploader video page 2023-05-04 18:00:06 -05:00
XnpioChV
135e0d7859 feat: Adding navigation on the url input of the video uploader page 2023-05-04 17:31:56 -05:00
XnpioChV
e20dedd0a5 feat: Allow to import transcripts from selected video 2023-05-04 17:31:55 -05:00
Pooja Kulkarni
359a5fd505 feat: Create new editor page for video upload 2023-05-04 17:31:54 -05:00
XnpioChV
da00ad7539 chore: All selection and save flow added 2023-05-04 17:01:42 -05:00
XnpioChV
23593b44fe chore: Error message on unselected video added & replace video button navigation added 2023-05-04 16:59:31 -05:00
XnpioChV
d2b3edad57 feat: Adding blockId to the Gallery. Adding cancel and next navigation 2023-05-04 16:58:36 -05:00
Raymond Zhou
adc21735fc fix: expandable textarea should save on blur (#325) 2023-05-04 17:17:14 -04:00
Kristin Aoki
34d6fcc552 feat: allow social share widget in libraries (#324) 2023-05-04 13:25:28 -04:00
Kristin Aoki
c49779a293 feat: add a button to return to studio (#322) 2023-05-03 10:35:02 -04:00
Kristin Aoki
6aaedfc500 feat: add social share widget (#323) 2023-05-03 09:55:31 -04:00
Raymond Zhou
d68bdf9dc6 feat: page errors indefinitely with no ancestor (#321) 2023-04-27 14:38:59 -04:00
kenclary
76ad7d8bda Merge pull request #262 from open-craft/pooja/bb7157-add-video-upload-feature
[BB-7157] Create new editor page for video upload
2023-04-26 13:41:37 -04:00
Pooja Kulkarni
bef6813d2c feat: Create new editor page for video upload 2023-04-26 11:36:26 -04:00
Kristin Aoki
bade92f613 feat: remove custom css with paragon upgrade (#319) 2023-04-26 09:55:40 -04:00
Kristin Aoki
21841fe09f fix: editable header width (#318) 2023-04-26 09:13:04 -04:00
Raymond Zhou
8676ba257c feat: default randomization to never (#317) 2023-04-25 16:14:40 -04:00
kenclary
eaab798da2 Merge pull request #278 from open-craft/chris/FAL-3375-video-selection-gallery
[FAL-3375] Feat: Video selection gallery page
2023-04-25 15:54:05 -04:00
XnpioChV
5695f92127 chore: Padding on gallery fixed 2023-04-21 18:06:10 -05:00
XnpioChV
36e56588cb test: Adding test for VideoGallery and SelectionModal 2023-04-21 18:06:09 -05:00
XnpioChV
b3bced9875 feat: New screens for loading, no videos and error states. Also tests added 2023-04-21 18:06:08 -05:00
XnpioChV
71efe876d3 style: adding more styles on the selection modal 2023-04-21 18:04:06 -05:00
XnpioChV
3b69958427 feat: Search and filters added 2023-04-21 18:04:05 -05:00
XnpioChV
b78e58cd2a feat: New request hook to fetch videos 2023-04-21 18:04:05 -05:00
XnpioChV
14504073e0 feat: Video selection page created
The SelectImageModal component has been refactored so that it can also be used on the video selection page; and all its child components.
Now this component is called SelectionModal and is used both for the image selector and in this new video selection screen.
The assets api has been used to get the videos.
2023-04-21 18:04:04 -05:00
Raymond Zhou
2a5f6795d3 feat: tinyMCE formatting and parsing (#311) 2023-04-21 11:42:10 -04:00
connorhaugh
802d328f4a feat: remove layered button (#314) 2023-04-21 11:04:23 -04:00
Kristin Aoki
c37e6957f6 fix: advanced editor not saving (#315) 2023-04-21 10:25:01 -04:00
Kristin Aoki
492ee27d8e fix: settings helper text font size (#313) 2023-04-21 09:19:39 -04:00
Raymond Zhou
80461755d1 feat: hint trash button should not be squished (#312) 2023-04-20 16:49:43 -04:00
Kristin Aoki
b674cd0cb0 feat: add no answer confirmation alert (#310) 2023-04-20 12:49:07 -04:00
Kristin Aoki
8f15480c28 feat: update problem descriptions (#309) 2023-04-18 13:23:18 -04:00
Kristin Aoki
4f1dc98ecc fix: settings font size and spacing (#308) 2023-04-18 11:56:20 -04:00
Kristin Aoki
405003045c fix: textArr join error in syntaxChecker (#307) 2023-04-14 16:28:16 -04:00
Kristin Aoki
1d2a4c212d feat: add xml linter to code mirror (#306) 2023-04-14 11:58:49 -04:00
Raymond Zhou
4b7b1c91ec feat: further number parse changes (#305) 2023-04-13 17:31:03 -04:00
Raymond Zhou
286d2209cb feat: XMLBuilder and XMLParser performing unwanted processing in encoding / parsing (#304) 2023-04-13 12:25:43 -04:00
Raymond Zhou
1e6d8b51e4 feat: show reset for advanced problem editor (#302) 2023-04-11 12:53:53 -04:00
connorhaugh
73a4b893f7 Feat parse solution as sibling (#303)
* fear: parse solution as siblings

* feat: add error resliancy to solution check.
2023-04-11 09:23:02 -04:00
connorhaugh
d58f349e1f feat: parse solution as siblings (#301)
this creates the ability for explanations to work if they are:

Not a child of a response type
Use an h2 tag for the explanation title
do not use the "Explanation" title.
2023-04-10 15:37:02 -04:00
connorhaugh
8a7dbdf4be fix: parse description/label as children (#300)
* fix: parse description/label as children

* fix: bounce problem tag attributes to advanced
2023-04-10 10:38:30 -04:00
Kristin Aoki
284139e247 feat: add check to sanitize answer ranges (#298) 2023-04-07 09:04:15 -04:00
Raymond Zhou
9a19711755 feat: revert group feedback expanded text field (#297) 2023-04-06 13:40:13 -04:00
Kristin Aoki
4cfc5a6ea6 feat: add alert to notify video id changes (#295) 2023-04-06 12:34:07 -04:00
connorhaugh
82cfa9897c feat: default scoring to 1, not zero. (#294)
Since every course has graded problems, every course author will be editing or creating problems, and every learner will need to receive points for any problem in a graded assignment in order to earn a passing grade. So if what I encountered and am reporting here is reproducible, this should probably be addressed before the problem editor feature is released.
2023-04-05 09:36:20 -04:00
Kristin Aoki
bd964854de fix: misspelling of explanation in description (#293) 2023-04-03 13:56:50 -04:00
Kristin Aoki
f99421f493 feat: add catch for script tags in question (#292) 2023-04-03 11:50:35 -04:00
connorhaugh
c2b67429d3 feat: add answer range (#291)
* feat: add answer range

* feat: add margin for doropdown

* fix: improve test converage
2023-03-30 10:34:45 -04:00
Omar Al-Ithawi
98ec415e2b fix: let make extract_translations find messages (#290)
Otherwise it'll complain for not finding any message.

`defineMessage` ensures that React i18n static code collector is able to
find the messages.

References
----------

This pull request is part of the [FC-0012 project](https://openedx.atlassian.net/l/cp/XGS0iCcQ) which is sparked by the [Translation Infrastructure update OEP-58](https://open-edx-proposals.readthedocs.io/en/latest/architectural-decisions/oep-0058-arch-translations-management.html#specification).

Check the links above for full information about the overall project.
2023-03-29 10:17:25 -04:00
kenclary
e7d69f4e5d Merge pull request #289 from openedx/kenclary/TNL-10535
fix: ErrorPage rewritten as functional component with injected i18n. TNL-10535
2023-03-24 16:40:10 -04:00
Ken Clary
f2ad93789f fix: ErrorPage rewritten as functional component with injected i18n. TNL-10535 2023-03-24 16:22:32 -04:00
connorhaugh
3bfa83220f feat: error on funky multiple choice tags. (#288)
This change increases the frequency at which odd tags (like shuffle) on multiple choice problems don't go to the visual editor.
Instead, they will now divert to the advanced editor. There might be other places where we need to follow a similar pattern, but we don't know those yet.
2023-03-24 10:05:21 -04:00
Kristin Aoki
7ef1963327 feat: add default advanced setting ui callouts (#285) 2023-03-23 17:37:13 -04:00
Ken Clary
4410f0a544 Merge branch 'keith/video-preview-widget' 2023-03-23 14:59:56 -04:00
Ken Clary
ed0aa88841 chore: fixup from rebase 2023-03-23 14:59:38 -04:00
Ken Clary
7080189a65 chore: fixup from rebase 2023-03-23 14:56:49 -04:00
Ken Clary
7a27b65a8c Merge branch 'keith/video-preview-widget' 2023-03-23 14:19:24 -04:00
Keith Grootboom
57a558f258 chore: use Hyperlink instead 2023-03-23 14:18:16 -04:00
Keith Grootboom
475c32463a refactor: review changes to use existing youtube function 2023-03-23 14:18:16 -04:00
Keith Grootboom
b4fb88c73c feat: add video preview widget to settings modal 2023-03-23 14:18:10 -04:00
kenclary
c7c0d5e100 Merge pull request #258 from open-craft/0x29a/video-duration-widget-improvements
feat: video duration section improvements [BD-12]
2023-03-23 11:47:31 -04:00
kenclary
21e01d1b77 Merge pull request #284 from open-craft/maxim/transcripts-section-improvements
feat: offer to import youtube transcripts dynamically
2023-03-23 11:18:31 -04:00
connorhaugh
df5af3efd9 feat: tolerance Setting Widget (#286)
* feat: tolerance Setting Widget

* fix: tolerance position and percent summary
2023-03-22 14:31:02 -04:00
Kristin Aoki
16003a7f4a feat: move explanation to main body of editor (#287) 2023-03-22 09:36:01 -04:00
Maxim Beder
6c8fca8113 test: add tests for VideoSettingsModal 2023-03-20 15:26:45 +01:00
Maxim Beder
363df38b1f feat: offer to import youtube transcripts dynamically
Before this commit, the popup message which offers to import transcripts
from YouTube would only appear after saving the video settings, and then
reopenning the editor.

With this commit, the popup appears dynamically, i.e. whenever a URL is
changed to a YouTube one, certain validations and check take place,
after which, if possible to do so, user will be offered to import
transcripts from the YouTube video.
2023-03-20 14:04:38 +01:00
Raymond Zhou
da9cb6054c Revert "feat: update switch to Advanced Editor warning (#259)" (#282)
This reverts commit 4a24d25f22.
2023-03-16 13:44:41 -04:00
Kristin Aoki
fa14365d54 feat: update text areas to expandable text areas (#274) 2023-03-16 13:17:01 -04:00
kenclary
84b9dd7e88 Merge pull request #277 from jansenk/jkantor/video-public-allowed-gate
feat: don't show video sharing checkbox unless waffle from studio is active
2023-03-15 16:21:03 -04:00
jansenk
97d0a74fef feat: call video_features cms endpoint for waffle status 2023-03-15 14:39:15 -04:00
jansenk
a895c28c4c feat: hide checkbox when video share not enabled for course 2023-03-15 14:39:15 -04:00
connorhaugh
2dc42e6a46 fix: show answer attempts spacing (#276) 2023-03-14 12:29:25 -04:00
Raymond Zhou
56621ca575 feat: remove general and group feedback (#273) 2023-03-13 12:25:38 -04:00
Mashal Malik
2ea3efed4b Update transifex api from v2 to v3 (#264) 2023-03-13 12:25:12 -04:00
connorhaugh
18fd63ab69 feat: expanding complex text area (#257) 2023-03-13 09:21:17 -04:00
kenclary
b917586fd8 Merge pull request #268 from openedx/kenclary/TNL-10507-1
fix: ProblemEditor useEffect at beginning of component; message for block failed to load. TNL-10507
2023-03-10 18:19:25 -05:00
Ken Clary
55b1e41898 fix: ProblemEditor useEffect at beginning of component; message for block failed to load. TNL-10507 2023-03-10 15:01:39 -05:00
Jesper Hodge
0ce9e4dfd3 fix: answer button accessibility (#271) 2023-03-10 14:18:21 -05:00
kenclary
b4edfe0bcb Merge pull request #270 from jansenk/jkantor/video-public
feat: Add "allow sharing" checkbox to video editor
2023-03-09 14:28:56 -05:00
jansenk
4467590a65 test: code review 2023-03-09 13:08:09 -05:00
jansenk
2be52871aa test: update snapshot 2023-03-09 11:38:17 -05:00
jansenk
968f48c55a feat: add checkbox for public video sharing field 2023-03-09 11:38:17 -05:00
Jeremy Ristau
54d4d2cca2 Merge pull request #269 from openedx/maintainership-phase1
chore: add missing maintainership files
2023-03-09 10:22:27 -05:00
Jeremy Ristau
07a84bc133 chore: add CODEOWNERS file to repo, point to tnl
This is related to Maintainership Pilot expectations. This CODEOWNERS file will notify teaching-and-learning team members of PR submissions, but there are currently no additional branch protections related to this ownership.
2023-03-09 09:58:32 -05:00
Jeremy Ristau
7fc149b882 chore: add catalog-info file for Open edX Backstage
This file populates Backstage info for the Open edX community. It helps identify that tnl is the owner of this component. This is in relation to Maintainership OEP-55.
2023-03-09 09:53:39 -05:00
Raymond Zhou
e6b532c71e Feat allow not select feedback for only multi select problem type (#267) 2023-03-08 15:18:57 -05:00
Raymond Zhou
b0c1e4d754 feat: clear save failed status when closing error (#266) 2023-03-07 13:27:55 -05:00
Raymond Zhou
b84c9c006e feat: fix time between attempts label (#263) 2023-03-02 16:46:05 -05:00
Kristin Aoki
5d77dddaf6 feat: add labels and blockquotes to clear format (#261) 2023-03-02 11:43:49 -05:00
connorhaugh
77f030c3fe fix: remove reset widget from advnaced editor 2023-03-02 10:20:22 -05:00
connorhaugh
7a8a182d5a fix: explantion data parse (#260) 2023-03-02 09:20:37 -05:00
Raymond Zhou
4a24d25f22 feat: update switch to Advanced Editor warning (#259) 2023-03-01 16:06:49 -05:00
Raymond Zhou
493ef9026e feat: numeric input UI to allow only correct (#256) 2023-03-01 13:02:27 -05:00
0x29a
897c440f26 test: test the new message, update the snapshot 2023-03-01 17:15:54 +01:00
0x29a
5b543ea93e feat: show "Custom" instead of "Total" when the widget is collapsed 2023-03-01 17:15:41 +01:00
0x29a
0fa18e4199 feat: outline and align right the total label 2023-03-01 16:31:20 +01:00
Kristin Aoki
c65f60ec10 feat: refactor tinymce editor to sharedComponents (#255) 2023-02-28 16:37:52 -05:00
Raymond Zhou
e0c5573c8d feat: tinyMCE config changes (#253) 2023-02-27 13:48:04 -05:00
connorhaugh
3c3361c765 fix: file upload on safari (#254)
* fix: file upload on safari

* fix: lint fix
2023-02-24 10:40:56 -05:00
Feanil Patel
a184ac981c Update standard workflow files. (#252)
* build: Creating a missing workflow file `self-assign-issue.yml`.

The .github/workflows/self-assign-issue.yml workflow is missing or needs an update to stay in
sync with the current standard for this workflow as defined in the
`.github` repo of the `openedx` GitHub org.

* build: Creating a missing workflow file `add-depr-ticket-to-depr-board.yml`.

The .github/workflows/add-depr-ticket-to-depr-board.yml workflow is missing or needs an update to stay in
sync with the current standard for this workflow as defined in the
`.github` repo of the `openedx` GitHub org.

* build: Creating a missing workflow file `add-remove-label-on-comment.yml`.

The .github/workflows/add-remove-label-on-comment.yml workflow is missing or needs an update to stay in
sync with the current standard for this workflow as defined in the
`.github` repo of the `openedx` GitHub org.

* build: Updating a missing workflow file `commitlint.yml`.

The .github/workflows/commitlint.yml workflow is missing or needs an update to stay in
sync with the current standard for this workflow as defined in the
`.github` repo of the `openedx` GitHub org.
2023-02-24 10:40:43 -05:00
Kristin Aoki
76aae38d3a fix: check for if an answer changes to false (#250) 2023-02-23 10:02:22 -05:00
Raymond Zhou
f18353e5fc feat: fix initial advanced problem type olx (#251) 2023-02-22 17:59:27 -05:00
connorhaugh
316f6f2850 feat: reset card link works (#249) 2023-02-17 13:53:09 -05:00
Kristin Aoki
70c0fc6dcf fix: settings col width in advanced view (#233) 2023-02-15 16:37:55 -05:00
Raymond Zhou
6156798e02 feat: move solution tag inside problem type tag (#248) 2023-02-15 12:53:02 -05:00
Raymond Zhou
3f9fc513cf feat: remove Hints widget from advanced editor (#247) 2023-02-15 12:52:52 -05:00
connorhaugh
61c99b9b40 feat: add error boundary (#246)
* feat: add error boundary
2023-02-14 15:21:43 -05:00
Raymond Zhou
529ec8ddf2 feat: fix checkbox colors for feedback (#244) 2023-02-14 12:34:55 -05:00
connorhaugh
b4f1676acf feat: add feedback link (#245)
For Problem Editor Beta
2023-02-14 10:20:26 -05:00
Kristin Aoki
eeaa0e3f68 fix: getGeneralFeedback selectedFeedback error (#243) 2023-02-10 14:54:56 -05:00
kenclary
0367ef776e Merge pull request #240 from openedx/kenclary/TNL-10433
fix: more correct parsing of settings, for scoring; use empty string instead of null for empty attempts. TNL-10433.
2023-02-10 12:01:10 -05:00
Raymond Zhou
eff3df7115 Feat allow singleselect to accept multiple correct answers (#241) 2023-02-10 11:31:52 -05:00
Kristin Aoki
a22ad54502 fix: update problem description in preview block (#242) 2023-02-10 11:00:49 -05:00
Ken Clary
38e262eee0 fix: more correct parsing of settings, for scoring; use empty string instead of null for empty attempts. TNL-10433. 2023-02-10 09:17:44 -05:00
connorhaugh
d69d3e1ce7 feat: Group, General Feedback Settings, Randomization
This Ticket adds three new settings widgets to the Problem Editor:
Randomization: This is a setting for advanced problems only which deals with python scripts.
General Feedback: This is feedback which is only applied to certain problem types for mass-adding feedback to incorrect problems.
Group Feedback: For certain problems, the user can provide specific feedback for a combination of specific answers.
2023-02-10 08:50:32 -05:00
kenclary
7c0309189f Merge pull request #238 from openedx/kenclary/TNL-10426
feat: multiple numericalresponse tags no longer special-cased against going to the advanced editor. TNL-10426.
2023-02-09 18:40:59 -05:00
Raymond Zhou
1282a72acf feat: green radio+checkbox only for valid Checker (#239) 2023-02-09 18:33:20 -05:00
Ken Clary
746cd7cc28 feat: multiple numericalresponse tags no longer special-cased against going to the advanced editor. TNL-10426. 2023-02-09 14:47:29 -05:00
Raymond Zhou
46427ee156 feat: make radio and checkbox green (#234) 2023-02-08 14:41:36 -05:00
Kristin Aoki
87aaa7f3ff fix: add empty answer to problem without answers (#235) 2023-02-07 20:04:16 -05:00
Kristin Aoki
613b8d16ae feat: update question parser to preserve text structure (#232) 2023-02-07 14:31:41 -05:00
Raymond Zhou
7e8f9a2f0c feat: change title when problem type changes (#231) 2023-02-06 13:11:59 -05:00
kenclary
65b663bfc6 Merge pull request #229 from openedx/kenclary/TNL-10413
fix: correct question parsing/building after use of styling. TNL-10413.
2023-02-06 10:17:02 -05:00
Ken Clary
ea6c2c6658 fix: correct question parsing/building after use of styling. TNL-10413. 2023-02-03 11:28:20 -05:00
Raymond Zhou
d978725c35 feat: react to OLX parser process number answers (#230) 2023-02-02 15:30:05 -05:00
Kristin Aoki
57dd03f40f feat: fix feedback box bugs (#227) 2023-02-02 13:51:58 -05:00
Kristin Aoki
796dd388f7 fix: selectTypeModal header z-index (#228) 2023-02-02 13:43:49 -05:00
Kristin Aoki
74b2b76beb fix: add check for question length for parser (#226) 2023-02-01 15:00:03 -05:00
kenclary
2f1072812d Merge pull request #224 from openedx/kenclary/TNL-10390
fix: correct css for text margins in question tinymce. TNL-10390.
2023-02-01 13:58:38 -05:00
Jesper Hodge
861b47b772 feat add olx solution support (#225)
This adds support for the tag in OLX and maps it to the description in the settings options on the ShowAnswer card.
https://2u-internal.atlassian.net/browse/TNL-10397
2023-02-01 13:28:14 -05:00
Ken Clary
b1201cdcef fix: correct css for text margins in question tinymce. TNL-10390. 2023-02-01 12:49:38 -05:00
Kristin Aoki
83d45a249a feat: add label button to question tinyMCE (#221) 2023-02-01 11:35:15 -05:00
kenclary
09d5ce35f3 Merge pull request #219 from openedx/kenclary/TNL-10395
fix: question field uses real placeholder instead of template. fixed blank question handling. TNL-10395, TNL-10411.
2023-01-31 13:31:36 -05:00
Kristin Aoki
787c4068f2 feat: add margin to prevent footer blocking content (#222) 2023-01-31 12:12:38 -05:00
Ken Clary
80f2689cc2 fix: question field uses real placeholder instead of template. fixed blank question handling. TNL-10395, TNL-10411. 2023-01-30 19:06:53 -05:00
kenclary
4a1bac3bdb Merge pull request #218 from openedx/TNL-10400
fix: give QuestionWidget Editor a minimum height. TNL-10400.
2023-01-27 10:37:04 -05:00
Ken Clary
84fe6605c2 fix: give QuestionWidget Editor a minimum height. TNL-10400. 2023-01-26 16:48:21 -05:00
kenclary
fa2387ae00 Merge pull request #217 from openedx/kenclary/TNL-10401
fix: multiple non-numeric problems of the same type should also route to the advanced editor. TNL-10401.
2023-01-26 15:57:21 -05:00
Ken Clary
3b9681618c fix: multiple non-numeric problems of the same type should also route to the advanced editor. TNL-10401. 2023-01-26 12:28:55 -05:00
Kristin Aoki
afec2865c0 fix: separate feedback onchange for each field (#214) 2023-01-26 09:48:43 -05:00
Jesper Hodge
83acc741f5 fix remove problem placeholder answer text (#215)
* refactor: rename hook correctly

* fix: problem templates
2023-01-25 15:02:35 -05:00
Kristin Aoki
2d669cbe3e feat: update styling in advanced problem editor (#216) 2023-01-25 10:42:30 -05:00
Kristin Aoki
656e8f8568 feat: update answer helper text (#213) 2023-01-25 10:10:08 -05:00
Kristin Aoki
be6aca8e8e feat: add function to remove empty hints (#211) 2023-01-24 13:12:41 -05:00
Jesper Hodge
1a2f175989 fix: problem editor styling (#212)
* fix: test name

* fix: make horizontal paddings 24px

* fix: space between widgets

* fix: remove settings heading

* fix: remove button hover effects

* fix: font size

* fix: make buttons small

* fix: change theme

* fix: reset buttons

* refactor: add Button component

* fix: hints widget

* fix: hints widget

* fix: tooltip

* fix: make settings fixed width

* fix: modal heading

* fix: center header text

* fix: modal header

* fix: settings fonts

* fix: settings fonts

* fix: fonts

* fix: padding

* fix: alignments

* fix: package.json

* fix: package.json

* fix: lint
2023-01-24 13:00:38 -05:00
Kristin Aoki
77afb7465b feat: update onclick to close modal + top scroll (#207) 2023-01-24 11:34:55 -05:00
Raymond Zhou
f135bd2b4a feat: align files with commit 5d52a28 and f9dff0 (#210)
This PR aims to fix the commit mistakes I made when trying to merge with a refactored fork. This will keep the changes I made in the refactor.
2023-01-24 09:43:01 -05:00
rayzhou-bit
acee24eaa7 Merge branch 'main' of https://github.com/openedx/frontend-lib-content-components 2023-01-23 15:42:18 -05:00
rayzhou-bit
b7c654399b fix: fix revert 2023-01-23 15:42:13 -05:00
rayzhou-bit
1557fabf9e Revert "Merge branch 'main' of github.com:rayzhou-bit/frontend-lib-content-components into rayzhou-bit-main"
This reverts commit f9dff0df85, reversing
changes made to 5d52a289dc.
2023-01-23 15:12:32 -05:00
rayzhou-bit
bafc3c8de8 Revert "feat: merge conflicts"
This reverts commit 73ec807dd3, reversing
changes made to 62cfecc456.
2023-01-23 14:51:21 -05:00
rayzhou-bit
73ec807dd3 feat: merge conflicts 2023-01-23 13:49:25 -05:00
rayzhou-bit
f9dff0df85 Merge branch 'main' of github.com:rayzhou-bit/frontend-lib-content-components into rayzhou-bit-main 2023-01-23 13:42:15 -05:00
kenclary
5d52a289dc Merge pull request #200 from openedx/kenclary/TNL-10332
fix: various style and UX fixes. TNL-10332.
2023-01-23 13:01:26 -05:00
Ken Clary
d3be3d4240 fix: Correct summary of number of attempts; new separator between attempts summary and points summary; hint text for both attempts and points settings; fixed some widget paddings; advanced problems do not show type setting, and type setting menu does not show advanced problems. TNL-10332. 2023-01-23 12:34:43 -05:00
Jesper Hodge
5aca835a4b Fix problem editor margins and borders (#203)
* fix: fix border and allow customizing of tinymce style

* fix: make tinymce widget look like on figma

* fix: update settingsoptions card border

* fix: header typography

* fix: spacings

* chore: update snapshots

* Update src/editors/containers/ProblemEditor/components/EditProblemView/SettingsWidget/index.jsx

Co-authored-by: Kristin Aoki <42981026+KristinAoki@users.noreply.github.com>

* Update src/editors/containers/ProblemEditor/components/EditProblemView/SettingsWidget/index.jsx

Co-authored-by: Kristin Aoki <42981026+KristinAoki@users.noreply.github.com>

* Update src/editors/containers/ProblemEditor/components/EditProblemView/index.jsx

Co-authored-by: Kristin Aoki <42981026+KristinAoki@users.noreply.github.com>

* Update src/editors/containers/ProblemEditor/components/EditProblemView/QuestionWidget/index.jsx

Co-authored-by: Kristin Aoki <42981026+KristinAoki@users.noreply.github.com>

* Update src/editors/containers/ProblemEditor/components/EditProblemView/QuestionWidget/index.jsx

Co-authored-by: Kristin Aoki <42981026+KristinAoki@users.noreply.github.com>

* Update src/editors/containers/ProblemEditor/components/EditProblemView/QuestionWidget/index.jsx

Co-authored-by: Kristin Aoki <42981026+KristinAoki@users.noreply.github.com>

* fix: html and react problems in problem editor

* chore: update snapshots

* chore: apply pr suggestions

* chore: fix test coverage

* chore: fix lint

* chore: fix tests

* chore: fix lint

Co-authored-by: Kristin Aoki <42981026+KristinAoki@users.noreply.github.com>
2023-01-20 15:59:30 -05:00
Kristin Aoki
d6d3363866 feat: make problem select and preview a fixed size (#204) 2023-01-20 15:05:53 -05:00
Kristin Aoki
f08a662136 feat: remove drag and drop from advanced problems (#206)
Drag and drop V2 is still available in studio via the "Advanced" component.
2023-01-20 14:22:33 -05:00
Raymond Zhou
9c4077f32d feat: set videoId to '' instead of null on load (#205) 2023-01-20 11:22:18 -05:00
rayzhou-bit
317cb48dbe feat: fix to updateDuration call 2023-01-20 05:44:22 -05:00
rayzhou-bit
62cfecc456 feat: fix input for updateDuration call 2023-01-19 16:25:08 -05:00
rayzhou-bit
f64d1bb127 feat: package-lock 2023-01-18 18:18:39 -05:00
rayzhou-bit
af94e15ffb feat: name changes 2023-01-18 18:08:07 -05:00
Kristin Aoki
5e037a7209 feat: change editor footer from sticky to fixed (#202) 2023-01-18 14:39:46 -05:00
Kristin Aoki
adb24ef9ea feat: change default question text (#199) 2023-01-18 12:50:26 -05:00
Jesper Hodge
9a8307001f add testability adr (#201)
* chore: hook ADR round 1

* chore: add adr number

* chore: revert changes that are not docs-related

* chore: revert changes

Co-authored-by: Ben Warzeski <bwarzeski@edx.org>
2023-01-18 11:44:50 -05:00
rayzhou-bit
34b7b3314c feat: revert to connect 2023-01-18 06:12:49 -05:00
Kristin Aoki
7d21a3d4c9 feat: change feedback and delete icons to outline variants (#198) 2023-01-17 15:41:11 -05:00
Jesper Hodge
b7c24d1e1a Fix answer box styling. TNL-10333 (#197)
* fix: use feedback icon with correct hover color

* fix: problem answer layout squishes delete button background

* fix: remove borders from textarea

* fix: textarea resize

* refactor: remove renderThing-antipattern in answer option

* fix: answer option feedback color

* fix: add second feedback box to all problem types

* refactor: move extra components out of answer option file

* fix: icon disappearing on hover when active

* fix: update snapshot

* fix: lint

* fix: add tests

* fix: add tests

* fix: snapshots

* Update src/editors/containers/ProblemEditor/components/EditProblemView/AnswerWidget/AnswerOption.jsx

Co-authored-by: Kristin Aoki <42981026+KristinAoki@users.noreply.github.com>

* fix: resolve discussions from PR

Co-authored-by: Kristin Aoki <42981026+KristinAoki@users.noreply.github.com>
2023-01-13 17:00:56 -05:00
connorhaugh
95ae45cce8 fix: update title on type select (#196)
* fix: update title on type select

* fix: lint fix

* fix: add test
2023-01-13 14:37:52 -05:00
Kristin Aoki
1a370c12d9 feat: update problem type titles + small ui fixes (#195) 2023-01-13 10:04:01 -05:00
Kristin Aoki
990e35bdc2 feat: remove question box branding + menu (#192)
This PR address the console.log errors from the question box tinyMCE, removing the tinyMCE branding and menu.
2023-01-12 15:30:06 -05:00
connorhaugh
2a9851544e Fix: advanced problem template reference (#194)
* fix: correctly reference advanced problem templates

* fix: remove console log
2023-01-12 13:23:02 -05:00
Raymond Zhou
aad7a6b706 feat: video editor fix to disappearing transcript (#178)
* feat: video editor fix to disappearing transcript
2023-01-12 12:51:56 -05:00
Jesper Hodge
574c2cc76a fix: cannot open problem editor because of typeerror undefined (#193)
* fix: cannot open problem editor because of typeerror

* fix: snapshots
2023-01-12 12:25:18 -05:00
kenclary
6d823e4e7c Merge pull request #187 from openedx/kenclary/TNL-10324
fix: match new frontend behavior with legacy behavior (zero problem attempts is zero attempts, null attempts is infinite); negative values disallowed and forced to zero. TNL-10324.
2023-01-12 10:40:47 -05:00
Ken Clary
390d620664 fix: match new frontend behavior with legacy behavior (zero problem attempts is zero attempts, null attempts is infinite); negative values disallowed and forced to zero. TNL-10324. 2023-01-12 10:31:13 -05:00
connorhaugh
6b2b5ac455 Update workflow 2023-01-12 09:56:59 -05:00
connorhaugh
70f5bb1080 build: pin semantic release to unblock pipeline (#191)
I saw that the release CI https://github.com/openedx/frontend-lib-content-components/actions/runs/3897027405/jobs/6654318273 was failing the release on [semantic-release]: node version >=18 is required. Found v16.19.0.

from https://github.com/semantic-release/semantic-release/releases/tag/v20.0.0 we learn that node v18 is now the minimum required version of node.

The version of semantic-release that runs in a repo is usually based on the relevant Github Acton workflow file for the release, defined in the repo itself.

I am pinning that version to 19.0.5 until the next node upgrade, as it seems we recently upgrade to node 16.
2023-01-12 09:31:31 -05:00
Kristin Aoki
5f5dc911da fix: change # destination to advance settings url (#189) 2023-01-12 09:22:37 -05:00
rayzhou-bit
e66863795c feat: tests argh 2023-01-11 20:14:03 -05:00
connorhaugh
9b2e284ee3 fix: remove test (#188) 2023-01-11 16:53:16 -05:00
Kristin Aoki
d23a790ad2 feat: allow feedback to copy when problem changed (#184) 2023-01-11 16:40:36 -05:00
connorhaugh
cf1daa3ba5 Feat add templates for default problems after problem select (#185)
https://2u-internal.atlassian.net/browse/TNL-10316 is the relevant bug.

Problems have default values when created using the select type page.
2023-01-11 14:26:46 -05:00
Jesper Hodge
2c6679fe06 Feat raw olx editing. TNL-10218 (#182)
* refactor: move CodeEditor to shared components and remove circular dependency

* feat: add code editor to problem editor

* fix: typo

* feat: add save function to raw olx editor and add highlighting

* feat: simplify and add tests to edit problem view

* feat: add tests to problem edit view

* fix: update raw editor tests

* fix: code editor tests

* fix: package-lock

* fix: lint
2023-01-11 14:23:06 -05:00
connorhaugh
f81b0ee925 docs: add period to module.config.js (#183) 2023-01-11 10:25:25 -05:00
kenclary
6a4ac3525f Merge pull request #180 from openedx/kenclary/TNL-10280
feat: confirmation dialog for closing any editor. TNL-10280.
2023-01-11 10:07:46 -05:00
Ken Clary
9ba0da04c3 feat: confirmation dialog for closing any editor. TNL-10280. 2023-01-11 09:58:43 -05:00
Kristin Aoki
af4cd55390 feat: unselect multiple choices type multi->single (#181) 2023-01-10 13:25:19 -05:00
connorhaugh
880d205cbb Feat: raw editor ingress and egress logic (#179)
* feat: conditional rendering of olx editor.

* fix: open the raw editor if advanced is chosen

* fix: add test fix

* feat: add button to switch visual->advanced

* fix: add tests + lint for visual->advanced button

* feat: revert to advanced if parser fails

* fix: improve coverage

* feat: add confirm dialog to switch

* fix: load settings with advanced

* fix: refactor + lint fix
2023-01-10 09:42:44 -05:00
rayzhou-bit
e946fb8711 feat: duration change first pass 2023-01-02 20:52:16 -05:00
Kristin Aoki
09bb1dab2b feat: add import transcripts from youtube (#176) 2022-12-23 10:54:07 -05:00
connorhaugh
2896393c53 feat: add problem type select
Co-authored-by: Kristin Aoki <42981026+KristinAoki@users.noreply.github.com>
Co-authored-by: Raymond Zhou <56318341+rayzhou-bit@users.noreply.github.com>
2022-12-22 16:52:21 -05:00
connorhaugh
83cbac6270 feat: doc updates for problem editor release 2022-12-20 15:31:14 -05:00
connorhaugh
8dea72de99 feat:problem editor
Co-authored-by: Farhaan Bukhsh <farhaan@opencraft.com>
Co-authored-by: Navin Karkera <navin@disroot.org>
Co-authored-by: Kaustav Banerjee <kaustav@opencraft.com>
2022-12-20 14:52:20 -05:00
connorhaugh
6f82e87574 feat: make delete and info icons outlines (#150)
* feat: make delete and info icons outlines

* feat:upgrade paragon
2022-12-14 11:44:41 -05:00
Kristin Aoki
f85d86f796 fix: left and right alignment (#167) 2022-12-13 15:04:53 -05:00
Zubair Shakoor
128b112af7 fix: -t flag added in pull translation command (#142) 2022-12-13 13:23:24 -05:00
Kristin Aoki
7daa2c2dba fix: vertical spacing between items (#166) 2022-12-13 13:20:15 -05:00
Kristin Aoki
b5be6f441c fix: add check if field is cleared (#165) 2022-12-12 14:18:25 -05:00
Kristin Aoki
07202c0518 feat: separate video id and url fields into two (#164) 2022-12-12 10:55:32 -05:00
Raymond Zhou
bcb3c3f7fb feat: parseTranscript to work with all languages (#162)
* feat: parseTranscript to work with all languages
2022-12-09 11:05:50 -05:00
Raymond Zhou
cc61e2944c feat: header overlap content when scrolling down (#161)
* feat: header overlap content when scrolling down
2022-12-07 15:24:48 -05:00
Kristin Aoki
01c3a42eb2 fix: transcript styling (#159) 2022-12-07 11:16:02 -05:00
kenclary
a15b894ec1 Merge pull request #160 from openedx/kenclary/TNL-10236
fix: editor title editing no longer tries to scale with input size. TNL-10236.
2022-12-07 11:10:21 -05:00
kenclary
014523731e Merge pull request #158 from openedx/kenclary/TNL-10235
fix: editor container titles are the same height whether displaying or editing. TNL-10235.
2022-12-07 11:01:38 -05:00
Ken Clary
f6574c6849 fix: editor title editing no longer tries to scale with input size. TNL-10236. 2022-12-07 10:31:29 -05:00
Ken Clary
3239810a7f fix: editor container titles are the same height whether displaying or editing. TNL-10235. 2022-12-07 10:05:33 -05:00
Kristin Aoki
6368ccf5ac feat: show view license link at all levels (#157) 2022-12-05 12:43:14 -05:00
Kristin Aoki
0b199b1f5d feat: add black canvas when current thumbnail is deleted (#156) 2022-12-02 16:47:00 -05:00
Raymond Zhou
73150817ca feat: update paragon to v20.21.0 (#155) 2022-12-02 12:54:45 -05:00
Kristin Aoki
f824ef0551 fix: license styling (#154) 2022-12-01 15:14:44 -05:00
connorhaugh
35b58a42b5 feat: left align widget "add" buttons (#153) 2022-12-01 12:20:29 -05:00
Kristin Aoki
6a2bafb402 fix: font/icon size and color (#152) 2022-11-30 15:31:39 -05:00
connorhaugh
e70800543a feat: increase spacing between video settings (#149) 2022-11-30 14:12:03 -05:00
kenclary
1360994fc5 Merge pull request #151 from edx/kenclary/TNL-10240
fix: correct various strings, including making them sentence-case. TNL-10240.
2022-11-30 12:45:04 -05:00
Ken Clary
c4a7b97b63 fix: correct various strings, including making them sentence-case. TNL-10240. 2022-11-30 12:36:42 -05:00
Kristin Aoki
7a356175e7 feat: update onBlur to set state to default values (#148) 2022-11-29 15:23:14 -05:00
Kristin Aoki
f38ae87c95 fix: length error on undefined fallback videos (#147) 2022-11-29 12:44:56 -05:00
Jeremy Ristau
08b5475548 feat: add CODEOWNERS file
feat: add CODEOWNERS file
2022-11-17 11:48:04 -05:00
Jeremy Ristau
f00f6174ac feat: add CODEOWNERS file
The repo should have a CODEOWNERS file, but this effort is specifically to help test a need to make an edx-platform file notify T&L when changes are made.  We are doing the testing on this edx repo that we own to minimize the churn and impact on edx-platform.

The scope of this effort includes:
# Create a CODEOWNERS file in this repo that calls out a single file as owned by a team.
# Submit a no-change PR to that file and ensure the team is notified
# Submit a no-change PR to another file in the repo and ensure the team is NOT notified
# Update the CODEOWNERS file to have T&L team as owners of the whole repo, instead of just one file (long term desired state)
2022-11-17 10:29:16 -05:00
Raymond Zhou
6c574ac18e feat: duration entree features (#143) 2022-11-15 16:22:29 -05:00
connorhaugh
ea50afc165 feat: update readme
adding this messaging to improve quality and trigger a release.
2022-11-15 12:17:30 -05:00
connorhaugh
3506db7c14 Feat improve transcript flow (#141)
https://2u-internal.atlassian.net/browse/TNL-10199
Rewrite of Transcripts widget to improve flow.
2022-11-15 12:03:04 -05:00
Kristin Aoki
8eab620b65 feat: auto-resize width of input and allow save onBlur (#144) 2022-11-14 13:24:11 -05:00
Kristin Aoki
af6e21e1fe fix: non-blocking UI bugs for video settings (#140) 2022-11-09 12:32:36 -05:00
Raymond Zhou
b4b31794af feat: allow license library content edit (#139)
* feat: allow license library content edit
2022-11-03 19:58:22 -04:00
kenclary
5f5250fd2c Merge pull request #131 from edx/kenclary/TNL-9823
feat: duration widget for video settings. TNL-9823.
2022-11-03 18:44:12 -04:00
Kristin Aoki
4187f6a884 feat: remove thumbnail widget from content libraries (#138) 2022-11-03 18:23:34 -04:00
Kristin Aoki
e5932ced27 feat: remove handout widget for content libraries (#137) 2022-11-03 18:16:04 -04:00
Ken Clary
a4f0a8f162 feat: duration widget for video settings. TNL-9823. 2022-11-03 17:55:05 -04:00
Raymond Zhou
79ae64b562 feat: license widget (#132) 2022-11-02 10:41:40 -04:00
kenclary
9c397d8802 Merge pull request #136 from edx/kenclary/TNL-10183
fix: filter for images in a null-safe/undefined-safe way. Fixes TNL-10183.
2022-10-27 10:52:40 -04:00
Ken Clary
c89ad83ed5 fix: filter for images in a null-safe/undefined-safe way. Fixes TNL-10183. 2022-10-26 20:32:52 -04:00
connorhaugh
bfb4de6b1a Fix: temp remove video preview text from settings editor (#134)
* fix: remove preview

* fix: unrelated remove removed fetchImages
2022-10-25 11:44:39 -04:00
Kristin Aoki
ca5846f1f6 fix: text editor save to use assets array instead of object 2022-10-25 10:42:26 -04:00
Kristin Aoki
8e55081ce1 feat: revert selection.url change and replaced with getting the relative path substring from the absolute url 2022-10-24 16:49:47 -04:00
Kristin Aoki
e6101ed3ac feat: update setAssetToStaticUrl to include all assets 2022-10-24 13:44:33 -04:00
Kristin Aoki
cb01ff17a0 feat: add handout widget 2022-10-19 12:17:59 -04:00
Kristin Aoki
8a2c337170 feat: update edx packages: pargon, frontend-platform,browserslist-config (#129) 2022-10-18 12:12:38 -04:00
Kristin Aoki
84f35dad40 feat: add automatic thumbnail resampling for thumbnail widget 2022-10-13 15:07:08 -04:00
Kristin Aoki
e9a123c16b feat: limit xblock title display 2022-10-13 12:44:56 -04:00
Kristin Aoki
36576903ea feat: thumbnail widget 2022-10-12 15:42:38 -04:00
Raymond Zhou
b035725344 feat: bump version (#127) 2022-10-12 15:11:29 -04:00
rayzhou-bit
72aa2dbe92 feat: bump version 2022-10-12 15:00:16 -04:00
Raymond Zhou
d2f07045f0 Feat video source integration (#125)
* feat: video source integration

Co-authored-by: KristinAoki <kaoki@2u.com>
2022-10-12 12:38:44 -04:00
kenclary
a7abab1236 Convert video settings from xblock metadata into the redux store. In-progress draft. TNL-10009. (#119)
* feat: Convert video settings from xblock metadata into the redux store. In-progress draft. TNL-10009.

Co-authored-by: rayzhou-bit <rzhou@2u.com>
2022-10-04 14:59:22 -04:00
Raymond Zhou
cc3a2d8b85 feat: videosource to backend (#118)
* feat: videosource to backend
2022-09-30 11:00:06 -04:00
Kristin Aoki
79ceaca8cc feat: update UI for video source widget (#120) 2022-09-28 11:34:56 -04:00
connorhaugh
45215ba504 Feat: full transcript widget (#117) 2022-09-27 14:09:29 -04:00
Kristin Aoki
ff636837cf feat: transcript parent widget component 2022-09-14 11:07:20 -04:00
connorhaugh
3f303a718d feat: unescape alphanumerics in code Editor (#112)
* feat: unescape alphanumerics in code Editor

* fix: replace disable with not render
2022-09-09 11:09:53 -04:00
Kristin Aoki
03a609ea98 feat: allow .ico image file types 2022-09-08 12:36:34 -04:00
Raymond Zhou
54d773a19a Feat shared widget componentries and layout (#107) 2022-09-07 12:22:27 -04:00
connorhaugh
720b41193f fix: intermittent raw loading error (#109)
As sometimes the html components are rendered downstream before the block is loaded, we need to handle the case of no block value being provided to the raw editor at the time of first render. Subsequent thunk actions will trigger re-renders to display the block.
2022-09-01 14:35:44 -04:00
Kristin Aoki
e5ea0a096c feat: change image absolute urls to relative urls 2022-08-31 12:03:07 -04:00
Kristin Aoki
2d427da80f feat: add tinyMCE functionality for drag resizing of images 2022-08-31 09:18:36 -04:00
Kristin Aoki
4e69fffbef feat: remove the ability to use image tools for content libraries 2022-08-30 16:40:17 -04:00
Kristin Aoki
36380edce4 feat: add codeMirror lineWrapping extension 2022-08-29 09:39:46 -04:00
Kristin Aoki
2b49304ecc feat: change static url to asset url in editor 2022-08-24 12:53:18 -04:00
Raymond Zhou
09110ec0b0 feat: run onImgLoad even on broken images (#102)
* feat: run onImgLoad even on broken images
2022-08-23 06:00:46 -04:00
connorhaugh
564dcb8ebc feat: improve raw HTML editing Experince. (#101)
* feat: add contents

* feat: add codemirror support to raw HTML editing

* feat: add test coverage

* fix: error

* fix: update codeeditor file path

* fix: update messages
2022-08-22 15:19:21 -04:00
Kristin Aoki
617f316f37 feat: allow relative urls for assets 2022-08-18 09:04:44 -04:00
Farhaan Bukhsh
405c3cf7e3 chore: Update the Readme for project (#98)
Signed-off-by: Farhaan Bukhsh <farhaan@opencraft.com>

Signed-off-by: Farhaan Bukhsh <farhaan@opencraft.com>
2022-08-17 11:13:40 -04:00
Kristin Aoki
d68c357a86 feat: resolve console errors regarding skin.css 2022-08-15 11:26:39 -04:00
Kristin Aoki
f86e1fe97e feat: update editor text to match studio styling 2022-08-12 10:47:11 -04:00
Raymond Zhou
476c450e6c Feat updates to code block button (#93)
* feat: updates to code block button

* feat: updates to code block button
2022-08-10 12:59:03 -04:00
Kristin Aoki
1b180de468 feat: add altText and isDecorative persistence for image editing 2022-08-10 09:17:35 -04:00
Kristin Aoki
d739bcbdb5 feat: Prevent image uploads larger than 10 MB and add spinner 2022-08-08 15:44:59 -04:00
Kristin Aoki
9b23731acc feat: Add functionality to resize image based on percentage (#94)
* feat: add image resizing with percents
2022-08-05 12:13:57 -04:00
Kristin Aoki
3821378dc1 Merge pull request #92 from edx/KristinAoki/tinymce-underline
fix: added underline option to toolbar. TNL-10003
2022-08-01 10:29:21 -04:00
KristinAoki
6be7433567 Merge branch 'KristinAoki/tinymce-underline' of https://github.com/edx/frontend-lib-content-components into KristinAoki/tinymce-underline 2022-08-01 09:15:35 -04:00
KristinAoki
cc7e502011 fix: added underline option to toolbar. TNL-10003 2022-08-01 09:11:01 -04:00
KristinAoki
a918da46c2 Add underline option to editor toolbar 2022-07-29 12:02:08 -04:00
Raymond Zhou
80dfc6ba15 feat: www gallery raw selection (#88)
* feat: www gallery raw selection
2022-07-22 10:58:55 -04:00
connorhaugh
224720c32e feat: reorganize dependancies (#91) 2022-07-11 16:19:31 -04:00
connorhaugh
9829a5d4ef feat: No - OP (#90) 2022-07-07 08:47:56 -04:00
connorhaugh
2473c9a875 chore: upgrade paragon dep (#89)
* chore: upgrade paragon dep

* fix: eslint config
2022-07-05 09:08:15 -04:00
Raymond Zhou
b047f7a2a8 feat: raw html editor (#86)
raw html editor
2022-06-28 14:25:09 -04:00
connorhaugh
b2fec70702 feat: swap relative Urls bool (#85) 2022-06-10 10:46:34 -04:00
connorhaugh
728dc58ac4 fix: Remove addition to URLs in text editor (#84) 2022-06-09 09:43:23 -04:00
connorhaugh
81fc4fca6f docs: adopt ADR 4 (#79) 2022-06-09 09:41:56 -04:00
connorhaugh
cc4e19cd2a fix: redirect to correct learning context (#83)
For https://2u-internal.atlassian.net/browse/TNL-9955
there are multiple instantiations of the text editor in different contexts, so we need to handle them upfront to not break either experience flow” or something along those lines…
2022-06-09 09:41:31 -04:00
Raymond Zhou
ca9f788838 feat: toolbar stays visible when scrolling (#82) 2022-05-27 12:40:20 -04:00
kenclary
0e523feb09 Merge pull request #81 from edx/kenclary/TNL-9943
fix: allow style tags inside html body, to match old editor behavior. TNL-9943.
2022-05-26 13:44:41 -04:00
kenclary
c02bb9df7f Merge branch 'main' into kenclary/TNL-9943 2022-05-25 17:23:07 -04:00
Ken Clary
5922e25b3d fix: allow style tags inside html body, to match old editor behavior. TNL-9943. 2022-05-25 15:15:43 -04:00
connorhaugh
3b859734fd feat: add simple code block and blockquote buttons (#80)
* feat: add simple code block and blockquote buttons

* fix: readaad context toolbar
2022-05-25 13:58:11 -04:00
connorhaugh
a74b2f2272 docs: V2 Content Editors ADR (#72)
This PR adds an overarching ADR for the V2 Content Editors framework, as well as several other, smaller, ADRs related to architectural abstractions.
2022-05-19 10:05:52 -04:00
connorhaugh
a3c50b2723 feat: allow all html tags (#77)
* feat: allow all html tags.
2022-05-18 13:24:13 -04:00
connorhaugh
94009e6ed7 fix: delete z index css (#78)
* fix: delete z index css

* fix: remove from index.css
2022-05-17 13:50:41 -04:00
connorhaugh
5aea213b8c feat: add autocreate new xblock (#67)
This PR adds an automated script for generating the boilerplate code for adding a new xblock editor. As well as documentation for how to do so.
2022-05-16 16:14:18 -04:00
kenclary
0d51b45636 Merge pull request #75 from edx/kenclary/TNL-9879
chore: npm update, using node 16 / npm 8. TNL-9879.
2022-05-15 10:18:28 -04:00
Ken Clary
d74f2b8ba9 chore: npm update, using node 16 / npm 8. TNL-9879. 2022-05-15 09:57:10 -04:00
Raymond Zhou
76dcd1a920 Feat alert users with feedback instead of disabling next/save buttons (#68)
* feat: implemented user feedback ErrorAlerts
2022-05-13 15:23:42 -04:00
Jawayria
b5480beaf8 feat: Add package-lock file version check (#73) 2022-05-13 14:55:29 -04:00
Tim McCormack
18e3012462 fix: Fix pull_translations by using correct CLI flag for languages (#74)
Docs: https://developers.transifex.com/docs/using-the-client

Apparently this CLI option changed from singular to plural at some point.
2022-05-13 13:03:26 -04:00
Ben Warzeski
564b953860 feat: video skeleton hooks (#65)
* feat: video skeleton hooks

* fix: lint

* Update src/editors/containers/VideoEditor/components/VideoSettingsModal/components/CollapsibleFormWidget.jsx

Co-authored-by: connorhaugh <49422820+connorhaugh@users.noreply.github.com>

Co-authored-by: connorhaugh <49422820+connorhaugh@users.noreply.github.com>
2022-05-09 14:23:21 -04:00
bszabo
964f00e563 Merge pull request #71 from edx/TNL-9888-text-editor-ADR
docs: ADR for text editor decisions May 2022
2022-05-06 11:39:56 -04:00
Bernard Szabo
46458b0f58 docs: TNL-9888 ADR for text editor decisions May 2022
Mostly notes on how to use TinyMCE to meet product requirements
Includes pull request feedback
Co-authored-by: Ben Warzeski <bwarzeski@2u.com>
2022-05-06 11:38:59 -04:00
Bernard Szabo
da6bdad9f0 docs: TNL-9888 ADR for text editor decisions May 2022
Mostly notes on how to use TinyMCE to meet product requirements
Includes pull request feedback
Co-authored-by: Ben Warzeski <bwarzeski@2u.com>
2022-05-06 11:24:18 -04:00
kenclary
121f28c535 Merge pull request #70 from edx/kenclary/TNL-9876
fix: code plugin uses 'HTML' label instead of icon. Fixes TNL-9876.
2022-05-04 18:25:12 -04:00
Ken Clary
91be5db424 fix: code plugin uses 'HTML' label instead of icon. Fixes TNL-9876. 2022-05-04 16:50:07 -04:00
Bernard Szabo
7b6bd31475 docs: fix typo and clarify no feature loss requirement
per Ray Zhou's PR feedback
2022-05-04 15:24:13 -04:00
Raymond Zhou
51f89bdc1e feat: The X Button in the Top Right Doesn't do anything. (#66)
* fix: tests

* fix: onClose test case
2022-05-04 14:20:23 -04:00
Bernard Szabo
5920096306 docs: ADR for text editor decisions May 2022
Mostly how to use and extended tinyMCE
2022-05-04 10:52:44 -04:00
connorhaugh
0d4688ce75 feat: remove codesample add code plugin (#64) 2022-04-19 12:53:33 -04:00
connorhaugh
081129b639 feat: no-op commit update readme (#63)
this is for the release to stage.
2022-04-19 11:54:25 -04:00
Raymond Zhou
5200897e1b Fix change to go back to 'fake' modal (#62)
* fix: rebase

* fix: tests

* fix: remove broken code button

* fix: test case with iff
2022-04-19 11:25:01 -04:00
kenclary
be12d11027 Merge pull request #61 from edx/kenclary/TNL-9874
fix: rearrange and add several tinymce toolbar items. Addresses TNL-9874.
2022-04-18 16:43:00 -04:00
Ken Clary
1999041cdf fix: rearrange and add several tinymce toolbar items. Addresses TNL-9874. 2022-04-18 15:37:47 -04:00
Julia Eskew
51558fd17c Add events sent to Segment for the save and cancel clicks for content editors. (#44)
* feat: Add events sent to Segment for the save and cancel clicks for a content editor.

Co-authored-by: rayzhou-bit <rzhou@edx.org>
2022-04-18 13:44:27 -04:00
kenclary
3620cac421 Merge pull request #60 from edx/kclary/TNL-9855
fix: enable ordered and unordered lists in text editor. Fixes TNL-9855.
2022-04-14 16:05:04 -04:00
Ken Clary
24baf8cbeb fix: enable ordered and unordered lists in text editor. Fixes TNL-9855. 2022-04-14 15:20:31 -04:00
Ben Warzeski
cb1c00bf3c Feat: Video skeleton (#58)
* pt1

* feat: video skeleton

* fix: update tests

* chore: update snapshots and linting

* fix: fix image context button

* feat: re-usable editor pattern

* fix: css and bad hook
2022-04-14 15:01:04 -04:00
bszabo
0edee6b4e0 Merge pull request #57 from edx/TNL-9804-make-small-code-stewardship-changes-to-frontend-lib-content-components
Tnl 9804 make small code stewardship changes to frontend lib content components
2022-04-11 18:12:19 -04:00
Bernard Szabo
0d60cd97a0 refactor: Fix footer aria descriptions
'screensaver' in Aria description messages changed to 'screen reader'
2022-04-11 16:33:19 -04:00
Raymond Zhou
553e4d8c04 fix: editor has double scrollbars (#59) 2022-04-11 12:26:39 -04:00
Bernard Szabo
48fcfb0e00 refactor: Preserve button default messages
description for aria labels may explicitly reference intended screensaver use, but label names and ids should remain unchanged
2022-04-08 16:55:14 -04:00
connorhaugh
33b2c6a660 feat: image context toolbar (#55)
Note: this removes the crop and rotate functionality from the toolbar, as it requires an image_proxy server or tinymce cloud. https://www.tiny.cloud/docs-4x/plugins/imagetools/#imagetools_proxy. We also need to create a follow up ticket to handle right click behavior.
This is that follow up ticket
2022-04-08 13:23:42 -04:00
Raymond Zhou
ef6ea6b617 fix: saving block title (#50)
* fix: saving block title

* fix: ben's suggestions
2022-04-08 12:45:49 -04:00
Bernard Szabo
d79ee29b96 refactor: consistent naming and default message for saveButton label
Prior to this change Aria message and default message were different
2022-04-07 16:12:13 -04:00
Bernard Szabo
26c0c78660 refactor: use consistent ordering for header/footer attributes 2022-04-07 16:12:13 -04:00
kenclary
bc3ef37dcf Merge pull request #53 from edx/kenclary/TNL-9832
fix: remove spurious hard-coded height for img-settings-form-container class. Fixes TNL-9832.
2022-04-07 15:49:06 -04:00
Ken Clary
e3236e9d95 fix: remove spurious hard-coded height for img-settings-form-container class. Fixes TNL-9832. 2022-04-07 15:44:47 -04:00
connorhaugh
c817e17d8a fix: update texteditor spacing (#49)
footer is now locked to bottom of page, and that text editor content scrolls, and that this works on resize of page.
2022-04-07 14:49:15 -04:00
Raymond Zhou
8104aa3152 fix: load block title (#51) 2022-04-07 12:47:35 -04:00
connorhaugh
c3d9109211 fix: remove merge artifiact (#48)
For repo health
2022-04-06 10:50:08 -04:00
Raymond Zhou
45758612a4 feat: empty gallery (#46)
Adds a new option to the behavior for when the gallery has no images in it.
2022-04-05 11:43:40 -04:00
connorhaugh
049b6a9211 Fix: release ci (#47)
* feat: add capabilities

* chore: more tests

* fix: use correct hook source for getDispatch

* fix: fix fetchImages data contract

* fix: make onClose method a callback for ImageUploadModal

* chore: lint and test updates

* chore: clean up propType warning

* fix: upload of file

* fix: error notifcations

* fix: improve test coverage

* fix: lint fixes

* fix: lint fix

* rebase: stept 1

* rebase: step 2

* fix: use correct hook source for getDispatch

* rebase: step 3

* rebase: step 4

* chore: clean up propType warning

* fix: upload of file

* rebase: step 5

* fix: improve test coverage

* fix: lint fixes

* fix: release CI

Co-authored-by: Ben Warzeski <bwarzeski@edx.org>
2022-04-04 09:17:34 -04:00
connorhaugh
fd35c1cb18 Feat: Add Request Alerts and upload file (#43)
Fixes to Upload data type, as well as adding in of two error alerts for upload and fetch.
Co-authored-by: Ben Warzeski <bwarzeski@edx.org>
Co-authored-by: Raymond Zhou <56318341+rayzhou-bit@users.noreply.github.com
2022-04-04 08:33:31 -04:00
Raymond Zhou
28516a0389 fix: text editor save (#45)
* fix: text editor save

* fix: update snapshot
2022-04-01 17:01:12 -04:00
Raymond Zhou
dca18b9b97 chore: i18n complete (#37)
* chore: i18n
2022-03-31 16:14:29 -04:00
Ben Warzeski
0f87a61639 feat: Select img api (#41)
* feat: add capabilities

* chore: more tests

* fix: use correct hook source for getDispatch

* fix: fix fetchImages data contract

* fix: make onClose method a callback for ImageUploadModal

* chore: lint and test updates

* chore: clean up propType warning

Co-authored-by: connorhaugh <chaugh21@amherst.edu>
2022-03-30 15:13:19 -04:00
Ben Warzeski
1a5497a5ae Feat select image modal (#38)
* feat: select image modal

* chore: fix module config
2022-03-24 16:16:38 -04:00
Ben Warzeski
09e9d865c2 Chore: Test coverage hunt (#36)
* chore: add brand mocking in gallery view

* feat: dev gallery app

* chore: link mock block ids to real block type api

* feat: image settings page features

* chore: more tests

* chore: keystore util and more testing

* chore: more tests

* chore: re-install lint plugin...

* chore: lint fixes

* chore: moar tests

* chore: remove brand from module.config and link gallery to edx.org brand
2022-03-24 11:15:32 -04:00
Ben Warzeski
284601d6d2 feat: image dimension lock logic update (#34) 2022-03-22 10:03:26 -04:00
Ben Warzeski
3b8a7780ac I18n example (#33)
* feat: i18n pt 1

* fix: prevent float calculations when updating dimensions
2022-03-17 14:08:46 -04:00
Ben Warzeski
a79da4cb2d feat: useState test util (#31) 2022-03-16 11:27:08 -04:00
Jawayria
9e84e0ecf0 fix: update dependencies (#32)
* fix: update dependencies

Co-authored-by: M Umar Khan <umar.khan@arbisoft.com>
2022-03-16 09:45:35 -04:00
Ben Warzeski
32e4d5f7a1 feat: Image settings feature and design completeness (#30)
* feat: devgallery api mode

* feat: dev gallery app

* chore: link mock block ids to real block type api

* feat: image settings page features

* fix: update tests

* fix: console message cleanup

* fix: test fixes from code walkthrough with ray
2022-03-15 16:32:07 -04:00
connorhaugh
4ae2d1230b feat: insert images into text editor html (#29)
This work adds the functionality that when an image is selected ineither the select image step of the image upload modal, or by using the toolbar inside tinymce, the requisite image is loaded into the settings page, and on the click of the save button, it is inserted into tinymce html.
2022-03-15 10:53:56 -04:00
connorhaugh
5258e93972 Test add requests redux tests (#24)
Add redux tests as well as get save functionality working.
2022-03-07 19:43:46 -05:00
connorhaugh
c4cd0c44ce test: update editor-level tests (#26)
To complete https://openedx.atlassian.net/browse/TNL-9601
2022-03-07 15:03:13 -05:00
Ben Warzeski
ac0d261e89 Gallery app (#27)
* feat: devgallery api mode

* chore: update react version

* feat: dev gallery app

* chore: fix component tests

* chore: lint fixes

* chore: link mock block ids to real block type api
2022-03-07 13:35:28 -05:00
Raymond Zhou
6f0f6296e4 test: cms api tests + extra lint (#25) 2022-03-01 11:32:03 -05:00
Ben Warzeski
9c9d3c8fdf feat: image upload skeleton (#22) 2022-03-01 11:17:03 -05:00
Raymond Zhou
f3d80995c5 test: cms urls (#23) 2022-02-25 19:24:05 -05:00
connorhaugh
7ccba63a85 test: add TextEditor Tests (#21) 2022-02-25 15:06:12 -05:00
Raymond Zhou
042246be86 Test: editor header component (#20)
* test: header tests start

* test: EditableHeader test ready for review 1

* test: editor header tests complete

* test: fixing up nits

* test: ben's corrections
2022-02-25 12:15:13 -05:00
Ben Warzeski
1a1900f213 feat: redux tests pt 1 (#19)
* feat: redux tests pt 1

* chore: add app thunkAction tests

* chore: resolve lint issues

* Update src/editors/data/redux/app/reducer.test.js

Co-authored-by: connorhaugh <49422820+connorhaugh@users.noreply.github.com>

Co-authored-by: connorhaugh <49422820+connorhaugh@users.noreply.github.com>
2022-02-24 16:59:30 -05:00
Ben Warzeski
2b0346fe84 chore: add tests for EditorHeader hooks (#18) 2022-02-23 11:55:53 -05:00
connorhaugh
6ca93a7297 test: add EditorFooter tests (#16)
* test: add EditorFooter tests
2022-02-22 13:24:15 -05:00
Ben Warzeski
d2e89d7b28 chore: rename lms service to cms (#15)
* chore: rename lms service to cms

* fix: remove typo

* fix: remove old selector
2022-02-22 12:59:37 -05:00
Ben Warzeski
362139edd2 feat: mock paragon and i18n library for tests 2022-02-22 12:45:30 -05:00
Ben Warzeski
5a1d71a62c !refactor: Breaking Change refactor use Redux. No release 2022-02-18 13:16:36 -05:00
connorhaugh
eef30348fd feat: add plugins including image-upload (#12)
This adds the image upload "plugin" and all the desired built-in plugins.
Per the following two tickets
https://openedx.atlassian.net/browse/TNL-9367
https://openedx.atlassian.net/browse/TNL-9475

I need to add the requisite plugins, as well as a framework for the image upload modal.
2022-02-14 12:33:49 -05:00
connorhaugh
2f931b5bdb fix: dont encode with uri twice (#10) 2022-01-27 12:39:19 -05:00
connorhaugh
d8c6b8dddd feat: Text Editor and V2 Editor Framework (#9)
Text Editor and V2 Editor Framework. Documentation to come.
2022-01-25 14:04:57 -05:00
kenclary
c93c5e986a Merge pull request #8 from edx/kenclary/release-fix
fix: correct project links in package.json
2021-12-15 11:34:36 -05:00
Ken Clary
bb7360ac93 fix: correct project links in package.json 2021-12-15 11:31:46 -05:00
kenclary
f7935deea0 Merge pull request #7 from edx/kenclary/release-fix
fix: correct(?) configuration of main branch for semantic-release.
2021-12-15 11:01:48 -05:00
Ken Clary
b98fe329af fix: correct(?) configuration of main branch for semantic-release. 2021-12-15 10:52:22 -05:00
kenclary
2bd75952f0 Merge pull request #6 from edx/kenclary/security
fix: update dependencies for security.
2021-12-14 17:22:23 -05:00
Ken Clary
f44017e5f4 fix: update dependencies for security. 2021-12-14 17:12:22 -05:00
kenclary
621f39f8ca Merge pull request #4 from edx/kenclary/release
fix: base semantic-release on main branch.
2021-12-13 11:30:33 -05:00
Ken Clary
159ecc84c3 fix: base semantic-release on main branch. 2021-12-09 16:06:16 -05:00
kenclary
e5a1694c90 Merge pull request #3 from edx/kclary/node
fix: update node to 16, using .nvmrc file, to support later versions of semantic-release.
2021-12-03 16:48:58 -05:00
Ken Clary
b37db23e8f fix: update node to 16, using .nvmrc file, to support later versions of semantic-release. 2021-12-03 12:10:06 -05:00
kenclary
105c29a67a Merge pull request #2 from edx/kenclary-initial
feat: setup repo as NPM package with edX standards.
2021-12-01 20:09:26 -05:00
Ken Clary
a48eb0aaf4 feat: setup repo as NPM package with edX standards. 2021-12-01 12:41:54 -05:00
kenclary
7142f89211 Merge pull request #1 from edx/kenclary-adr
Create 0001-library-adr.rst
2021-11-23 09:02:46 -05:00
kenclary
f547e5ed1f Create 0001-library-adr.rst
Draft of ADR for repo.
2021-11-22 16:52:19 -05:00
David Joy
9ed78de9e2 Initial commit 2021-11-19 14:28:50 -05:00
944 changed files with 84277 additions and 14525 deletions

6
.env
View File

@@ -1,3 +1,4 @@
APP_ID='authoring'
NODE_ENV='production'
ACCESS_TOKEN_COOKIE_NAME=''
BASE_URL=''
@@ -34,12 +35,13 @@ ENABLE_UNIT_PAGE=false
ENABLE_ASSETS_PAGE=false
ENABLE_VIDEO_UPLOAD_PAGE_LINK_IN_CONTENT_DROPDOWN=false
ENABLE_TAGGING_TAXONOMY_PAGES=true
ENABLE_CERTIFICATE_PAGE=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_HOME_PAGE_COURSE_API_V2=true
ENABLE_CHECKLIST_QUALITY=''
ENABLE_GRADING_METHOD_IN_PROBLEMS=false
LIBRARY_SUPPORTED_BLOCKS="problem,video,html"

View File

@@ -1,3 +1,4 @@
APP_ID='authoring'
NODE_ENV='development'
ACCESS_TOKEN_COOKIE_NAME='edx-jwt-cookie-header-payload'
BASE_URL='http://localhost:2001'
@@ -35,6 +36,7 @@ ENABLE_TEAM_TYPE_SETTING=false
ENABLE_UNIT_PAGE=false
ENABLE_ASSETS_PAGE=false
ENABLE_VIDEO_UPLOAD_PAGE_LINK_IN_CONTENT_DROPDOWN=true
ENABLE_CERTIFICATE_PAGE=true
ENABLE_NEW_VIDEO_UPLOAD_PAGE=true
ENABLE_TAGGING_TAXONOMY_PAGES=true
BBB_LEARN_MORE_URL=''
@@ -42,7 +44,7 @@ HOTJAR_APP_ID=''
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_HOME_PAGE_COURSE_API_V2=true
ENABLE_CHECKLIST_QUALITY=true
ENABLE_GRADING_METHOD_IN_PROBLEMS=false
LIBRARY_SUPPORTED_BLOCKS="problem,video,html"

View File

@@ -1,3 +1,4 @@
APP_ID='authoring'
ACCESS_TOKEN_COOKIE_NAME='edx-jwt-cookie-header-payload'
BASE_URL='http://localhost:2001'
CREDENTIALS_BASE_URL='http://localhost:18150'
@@ -31,9 +32,11 @@ ENABLE_TEAM_TYPE_SETTING=false
ENABLE_UNIT_PAGE=true
ENABLE_ASSETS_PAGE=false
ENABLE_VIDEO_UPLOAD_PAGE_LINK_IN_CONTENT_DROPDOWN=true
ENABLE_CERTIFICATE_PAGE=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
LIBRARY_SUPPORTED_BLOCKS="problem,video,html"

View File

@@ -11,8 +11,9 @@ module.exports = createConfig(
}],
'template-curly-spacing': 'off',
'react-hooks/exhaustive-deps': 'off',
indent: ['error', 2],
'no-restricted-exports': 'off',
// There is no reason to disallow this syntax anymore; we don't use regenerator-runtime in new browsers
'no-restricted-syntax': 'off',
},
settings: {
// Import URLs should be resolved using aliases

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

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

View File

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

2
.nvmrc
View File

@@ -1 +1 @@
18
20

View File

@@ -26,6 +26,7 @@
"scss/at-rule-no-unknown": true,
"scss/at-import-partial-extension": null,
"scss/comment-no-empty": null,
"import-notation": "string",
"property-no-unknown": [true, {
"ignoreProperties": ["xs", "sm", "md", "lg", "xl", "xxl"]
}],

View File

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

View File

@@ -35,13 +35,12 @@ pull_translations:
cd src/i18n/messages \
&& atlas pull $(ATLAS_OPTIONS) \
translations/frontend-component-ai-translations/src/i18n/messages:frontend-component-ai-translations \
translations/frontend-lib-content-components/src/i18n/messages:frontend-lib-content-components \
translations/frontend-platform/src/i18n/messages:frontend-platform \
translations/paragon/src/i18n/messages:paragon \
translations/frontend-component-footer/src/i18n/messages:frontend-component-footer \
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
$(intl_imports) frontend-component-ai-translations frontend-platform paragon frontend-component-footer frontend-app-course-authoring
# This target is used by Travis.
validate-no-uncommitted-package-lock-changes:
@@ -54,7 +53,7 @@ validate:
npm run i18n_extract
npm run lint -- --max-warnings 0
npm run types
npm run test
npm run test:ci
npm run build
.PHONY: validate.ci

View File

@@ -1,5 +1,5 @@
frontend-app-course-authoring
#############################
frontend-app-authoring
######################
|license-badge| |status-badge| |codecov-badge|
@@ -7,9 +7,9 @@ frontend-app-course-authoring
Purpose
*******
This is the Course Authoring micro-frontend, currently under development by `2U <https://2u.com>`_.
This implements most of the frontend for **Open edX Studio**, allowing authors to create and edit courses, libraries, and their learning components.
Its purpose is to provide both a framework and UI for new or replacement React-based authoring features outside ``edx-platform``. You can find the current set described below.
A few parts of Studio still default to the `"legacy" pages defined in edx-platform <https://github.com/openedx/edx-platform/tree/master/cms>`_, but those are rapidly being deprecated and replaced with the React- and Paragon-based pages defined here.
Getting Started
@@ -18,51 +18,87 @@ Getting Started
Prerequisites
=============
The `devstack`_ is currently recommended as a development environment for your
new MFE. If you start it with ``make dev.up.lms`` that should give you
everything you need as a companion to this frontend.
Note that it is also possible to use `Tutor`_ to develop an MFE. You can refer
to the `relevant tutor-mfe documentation`_ to get started using it.
.. _Devstack: https://github.com/openedx/devstack
`Tutor`_ is currently recommended as a development environment for the Authoring
MFE. Most likely, it already has this MFE configured; however, you'll need to
make some changes in order to run it in development mode. You can refer
to the `relevant tutor-mfe documentation`_ for details, or follow the quick
guide below.
.. _Tutor: https://github.com/overhangio/tutor
.. _relevant tutor-mfe documentation: https://github.com/overhangio/tutor-mfe#mfe-development
Configuration
=============
All features that integrate into the edx-platform CMS require that the ``COURSE_AUTHORING_MICROFRONTEND_URL`` Django setting is set in the CMS environment and points to this MFE's deployment URL. This should be done automatically if you are using devstack or tutor-mfe.
Cloning and Setup
=================
Cloning and Startup
===================
1. Clone your new repo:
.. code-block:: bash
1. Clone the repo:
git clone https://github.com/openedx/frontend-app-authoring.git
``git clone https://github.com/openedx/frontend-app-course-authoring.git``
2. Use node v20.x.
2. Use node v18.x.
The current version of the micro-frontend build scripts supports node 20.
Using other major versions of node *may* work, but this is unsupported. For
convenience, this repository includes an ``.nvmrc`` file to help in setting the
correct node version via `nvm <https://github.com/nvm-sh/nvm>`_.
The current version of the micro-frontend build scripts support node 18.
Using other major versions of node *may* work, but this is unsupported. For
convenience, this repository includes an .nvmrc file to help in setting the
correct node version via `nvm use`_.
3. Stop the Tutor devstack, if it's running: ``tutor dev stop``
3. Install npm dependencies:
4. Next, we need to tell Tutor that we're going to be running this repo in
development mode, and it should be excluded from the ``mfe`` container that
otherwise runs every MFE. Run this:
``cd frontend-app-course-authoring && npm install``
.. code-block:: bash
tutor mounts add /path/to/frontend-app-authoring
4. Start the dev server:
5. Start Tutor in development mode. This command will start the LMS and Studio,
and other required MFEs like ``authn`` and ``account``, but will not start
the Authoring MFE, which we're going to run on the host instead of in a
container managed by Tutor. Run:
``npm start``
.. code-block:: bash
tutor dev start lms cms mfe
The dev server is running at `http://localhost:2001 <http://localhost:2001>`_.
or whatever port you setup.
Startup
=======
1. Install npm dependencies:
.. code-block:: bash
cd frontend-app-authoring && npm ci
2. Start the dev server:
.. code-block:: bash
npm run dev
Then you can access the app at http://apps.local.openedx.io:2001/course-authoring/home
Troubleshooting
---------------
* If you see an "Invalid Host header" error, then you're probably using a different domain name for your devstack such as
``local.edly.io`` or ``local.overhang.io`` (not the new recommended default, ``local.openedx.io``). In that case, run
these commands to update your devstack's domain names:
.. code-block:: bash
tutor dev stop
tutor config save --set LMS_HOST=local.openedx.io --set CMS_HOST=studio.local.openedx.io
tutor dev launch -I --skip-build
tutor dev stop authoring # We will run this MFE on the host
* If tutor-mfe is not starting the authoring MFE in development mode (eg. `tutor dev start authoring` fails), it may be due to
using a tutor version that expects the MFE name to be frontend-app-course-authoring (the previous name of this repo). To fix
this, you can rename the cloned repo directory to frontend-app-course-authoring. More information can be found in
[this forum post](https://discuss.openedx.org/t/repo-rename-frontend-app-course-authoring-frontend-app-authoring/13930/2)
Features
@@ -145,10 +181,6 @@ Feature Description
When a corresponding waffle flag is set, upon editing a block in Studio, the view is rendered by this MFE instead of by the XBlock's authoring view. The user remains in Studio.
.. note::
The new editors themselves are currently implemented in a repository outside ``openedx``: `frontend-lib-content-components <https://github.com/edx/frontend-lib-content-components/>`_, a dependency of this MFE. This repository is slated to be moved to the ``openedx`` org, however.
Feature: New Proctoring Exams View
==================================
@@ -264,6 +296,22 @@ In additional to the standard settings, the following local configuration items
Tagging/Taxonomy functionality.
Feature: Libraries V2/Legacy Tabs
=================================
Configuration
-------------
In additional to the standard settings, the following local configurations can be set to switch between different library modes:
* ``MEILISEARCH_ENABLED``: Studio setting which is enabled when the `meilisearch plugin`_ is installed.
* ``edx-platform`` Waffle flags:
* ``contentstore.new_studio_mfe.disable_legacy_libraries``: this feature flag must be OFF to show legacy Libraries V1
* ``contentstore.new_studio_mfe.disable_new_libraries``: this feature flag must be OFF to show Content Libraries V2
.. _meilisearch plugin: https://github.com/open-craft/tutor-contrib-meilisearch
Developing
**********
@@ -296,8 +344,8 @@ The production build is created with ``npm run build``.
:target: https://travis-ci.com/edx/frontend-app-course-authoring
.. |Codecov| image:: https://codecov.io/gh/edx/frontend-app-course-authoring/branch/master/graph/badge.svg
:target: https://codecov.io/gh/edx/frontend-app-course-authoring
.. |license| image:: https://img.shields.io/npm/l/@edx/frontend-app-course-authoring.svg
:target: @edx/frontend-app-course-authoring
.. |license| image:: https://img.shields.io/npm/l/@edx/frontend-app-authoring.svg
:target: @edx/frontend-app-authoring
Internationalization
====================

View File

@@ -4,11 +4,11 @@
apiVersion: backstage.io/v1alpha1
kind: Component
metadata:
name: 'frontend-app-course-authoring'
description: "The frontend (MFE) for Open edX Course Authoring (aka Studio)"
name: 'frontend-app-authoring'
description: "The frontend (MFE) for Open edX Authoring (aka Studio)"
links:
- url: "https://github.com/openedx/frontend-app-course-authoring"
title: "Frontend app course authoring"
- url: "https://github.com/openedx/frontend-app-authoring"
title: "Frontend app authoring"
icon: "Web"
annotations:
openedx.org/arch-interest-groups: ""

15761
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,10 +1,10 @@
{
"name": "@edx/frontend-app-course-authoring",
"name": "@edx/frontend-app-authoring",
"version": "0.1.0",
"description": "Frontend application template",
"repository": {
"type": "git",
"url": "git+https://github.com/openedx/frontend-app-course-authoring.git"
"url": "git+https://github.com/openedx/frontend-app-authoring.git"
},
"browserslist": [
"extends @edx/browserslist-config"
@@ -13,12 +13,14 @@
"build": "fedx-scripts webpack",
"i18n_extract": "fedx-scripts formatjs extract",
"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",
"lint": "npm run stylelint && fedx-scripts eslint --ext .js --ext .jsx --ext .ts --ext .tsx .",
"lint:fix": "npm run stylelint -- --fix && fedx-scripts eslint --fix --ext .js --ext .jsx --ext .ts --ext .tsx .",
"snapshot": "TZ=UTC fedx-scripts jest --updateSnapshot",
"start": "fedx-scripts webpack-dev-server --progress",
"start:with-theme": "paragon install-theme && npm start && npm install",
"dev": "PUBLIC_PATH=/authoring/ MFE_CONFIG_API_URL='http://localhost:8000/api/mfe_config/v1' fedx-scripts webpack-dev-server --progress --host apps.local.openedx.io",
"test": "TZ=UTC fedx-scripts jest --coverage --passWithNoTests",
"test:ci": "TZ=UTC fedx-scripts jest --silent --coverage --passWithNoTests",
"types": "tsc --noEmit"
},
"husky": {
@@ -28,32 +30,30 @@
},
"author": "edX",
"license": "AGPL-3.0",
"homepage": "https://github.com/openedx/frontend-app-course-authoring#readme",
"homepage": "https://github.com/openedx/frontend-app-authoring#readme",
"publishConfig": {
"access": "public"
},
"bugs": {
"url": "https://github.com/openedx/frontend-app-course-authoring/issues"
"url": "https://github.com/openedx/frontend-app-authoring/issues"
},
"dependencies": {
"@codemirror/lang-html": "^6.0.0",
"@codemirror/lang-xml": "^6.0.0",
"@codemirror/lint": "^6.2.1",
"@codemirror/state": "^6.0.0",
"@codemirror/view": "^6.0.0",
"@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": "^2.0.0",
"@edx/frontend-component-footer": "^13.0.2",
"@edx/frontend-component-header": "^5.1.0",
"@edx/brand": "npm:@openedx/brand-openedx@^1.2.3",
"@edx/browserslist-config": "1.2.0",
"@edx/frontend-component-footer": "^14.1.0",
"@edx/frontend-component-header": "^5.6.0",
"@edx/frontend-enterprise-hotjar": "^2.0.0",
"@edx/frontend-lib-content-components": "^2.1.7",
"@edx/frontend-platform": "7.0.1",
"@edx/frontend-platform": "^8.0.3",
"@edx/openedx-atlas": "^0.6.0",
"@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",
@@ -64,62 +64,67 @@
"@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",
"@openedx/frontend-build": "^14.0.14",
"@openedx/frontend-plugin-framework": "^1.2.1",
"@openedx/paragon": "^22.8.1",
"@redux-devtools/extension": "^3.3.0",
"@reduxjs/toolkit": "1.9.7",
"@tanstack/react-query": "4.36.1",
"classnames": "2.2.6",
"core-js": "3.8.1",
"@tinymce/tinymce-react": "^3.14.0",
"classnames": "2.5.1",
"codemirror": "^6.0.0",
"email-validator": "2.0.4",
"fast-xml-parser": "^4.0.10",
"file-saver": "^2.0.5",
"formik": "2.2.6",
"formik": "2.4.6",
"frontend-components-tinymce-advanced-plugins": "^1.0.3",
"jszip": "^3.10.1",
"lodash": "4.17.21",
"meilisearch": "^0.38.0",
"moment": "2.29.4",
"meilisearch": "^0.41.0",
"moment": "2.30.1",
"moment-shortformat": "^2.1.0",
"npm": "^10.8.1",
"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-onclickoutside": "^6.13.0",
"react-redux": "7.2.9",
"react-responsive": "9.0.2",
"react-router": "6.16.0",
"react-router-dom": "6.16.0",
"react-router": "6.27.0",
"react-router-dom": "6.27.0",
"react-select": "5.8.0",
"react-textarea-autosize": "^8.4.1",
"react-textarea-autosize": "^8.5.3",
"react-transition-group": "4.4.5",
"redux": "4.0.5",
"regenerator-runtime": "0.13.7",
"redux-logger": "^3.0.6",
"redux-thunk": "^2.4.1",
"reselect": "^4.1.5",
"start": "^5.1.0",
"tinymce": "^5.10.4",
"universal-cookie": "^4.0.4",
"uuid": "^3.4.0",
"xmlchecker": "^0.1.0",
"yup": "0.31.1"
},
"devDependencies": {
"@edx/browserslist-config": "1.2.0",
"@edx/react-unit-test-utils": "^2.0.0",
"@edx/reactifex": "^1.0.3",
"@edx/stylelint-config-edx": "2.3.0",
"@edx/react-unit-test-utils": "3.0.0",
"@edx/stylelint-config-edx": "2.3.3",
"@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",
"@types/lodash": "^4.17.7",
"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",
"jest-expect-message": "^1.1.3",
"react-test-renderer": "17.0.2",
"reactifex": "1.1.1",
"ts-loader": "^9.5.0"
},
"peerDependencies": {
"decode-uri-component": ">=0.2.2"
"redux-mock-store": "^1.5.4"
}
}

View File

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

View File

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

View File

@@ -3,7 +3,7 @@
"version": "0.1.0",
"description": "Learning Assistant configuration for courses using it",
"peerDependencies": {
"@edx/frontend-app-course-authoring": "*",
"@edx/frontend-app-authoring": "*",
"@edx/frontend-platform": "*",
"@openedx/paragon": "*",
"prop-types": "*",
@@ -11,7 +11,7 @@
"yup": "*"
},
"peerDependenciesMeta": {
"@edx/frontend-app-course-authoring": {
"@edx/frontend-app-authoring": {
"optional": true
}
}

View File

@@ -3,10 +3,10 @@ import { useDispatch, useSelector } from 'react-redux';
import { camelCase } from 'lodash';
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 SelectableBox from 'CourseAuthoring/editors/sharedComponents/SelectableBox';
import AppSettingsModal from 'CourseAuthoring/pages-and-resources/app-settings-modal/AppSettingsModal';
import { useModel } from 'CourseAuthoring/generic/model-store';
import Loading from 'CourseAuthoring/generic/Loading';

View File

@@ -3,8 +3,7 @@
"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-app-authoring": "*",
"@edx/frontend-platform": "*",
"@openedx/paragon": "*",
"@reduxjs/toolkit": "*",
@@ -16,7 +15,7 @@
"yup": "*"
},
"peerDependenciesMeta": {
"@edx/frontend-app-course-authoring": {
"@edx/frontend-app-authoring": {
"optional": true
}
}

View File

@@ -1,69 +1,176 @@
import React from 'react';
import { useEffect, useState, useRef } from 'react';
import PropTypes from 'prop-types';
import * as Yup from 'yup';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { useIntl } from '@edx/frontend-platform/i18n';
import { useDispatch, useSelector } from 'react-redux';
import { Hyperlink } from '@openedx/paragon';
import { useModel } from 'CourseAuthoring/generic/model-store';
import {
ActionRow, Alert, Badge, Form, Hyperlink, ModalDialog, StatefulButton,
} from '@openedx/paragon';
import { Info } from '@openedx/paragon/icons';
import { updateModel, useModel } from 'CourseAuthoring/generic/model-store';
import { RequestStatus } from 'CourseAuthoring/data/constants';
import FormSwitchGroup from 'CourseAuthoring/generic/FormSwitchGroup';
import { useAppSetting } from 'CourseAuthoring/utils';
import AppSettingsModal from 'CourseAuthoring/pages-and-resources/app-settings-modal/AppSettingsModal';
import Loading from 'CourseAuthoring/generic/Loading';
import PermissionDeniedAlert from 'CourseAuthoring/generic/PermissionDeniedAlert';
import ConnectionErrorAlert from 'CourseAuthoring/generic/ConnectionErrorAlert';
import { useAppSetting, useIsMobile } from 'CourseAuthoring/utils';
import { getLoadingStatus, getSavingStatus } from 'CourseAuthoring/pages-and-resources/data/selectors';
import { updateSavingStatus } from 'CourseAuthoring/pages-and-resources/data/slice';
import messages from './messages';
const ORASettings = ({ intl, onClose }) => {
const ORASettings = ({ onClose }) => {
const dispatch = useDispatch();
const { formatMessage } = useIntl();
const alertRef = useRef(null);
const updateSettingsRequestStatus = useSelector(getSavingStatus);
const loadingStatus = useSelector(getLoadingStatus);
const isMobile = useIsMobile();
const modalVariant = isMobile ? 'dark' : 'default';
const appId = 'ora_settings';
const appInfo = useModel('courseApps', appId);
const [enableFlexiblePeerGrade, saveSetting] = useAppSetting(
'forceOnFlexiblePeerOpenassessments',
);
const initialFormValues = { enableFlexiblePeerGrade };
const [formValues, setFormValues] = useState(initialFormValues);
const [saveError, setSaveError] = useState(false);
const submitButtonState = updateSettingsRequestStatus === RequestStatus.IN_PROGRESS ? 'pending' : 'default';
const handleSettingsSave = (values) => saveSetting(values.enableFlexiblePeerGrade);
const title = (
<div>
<p>{intl.formatMessage(messages.heading)}</p>
<div className="pt-3">
<Hyperlink
className="text-primary-500 small"
destination={appInfo.documentationLinks?.learnMoreConfiguration}
target="_blank"
rel="noreferrer noopener"
>
{intl.formatMessage(messages.ORASettingsHelpLink)}
</Hyperlink>
</div>
</div>
);
const handleSubmit = async (event) => {
let success = true;
event.preventDefault();
success = success && await handleSettingsSave(formValues);
await setSaveError(!success);
if ((initialFormValues.enableFlexiblePeerGrade !== formValues.enableFlexiblePeerGrade) && success) {
success = await dispatch(updateModel({
modelType: 'courseApps',
model: {
id: appId, enabled: formValues.enableFlexiblePeerGrade,
},
}));
}
!success && alertRef?.current.scrollIntoView(); // eslint-disable-line @typescript-eslint/no-unused-expressions
};
const handleChange = (e) => {
setFormValues({ enableFlexiblePeerGrade: e.target.checked });
};
useEffect(() => {
if (updateSettingsRequestStatus === RequestStatus.SUCCESSFUL) {
dispatch(updateSavingStatus({ status: '' }));
onClose();
}
}, [updateSettingsRequestStatus]);
const renderBody = () => {
switch (loadingStatus) {
case RequestStatus.SUCCESSFUL:
return (
<>
{saveError && (
<Alert variant="danger" icon={Info} ref={alertRef}>
<Alert.Heading>
{formatMessage(messages.errorSavingTitle)}
</Alert.Heading>
{formatMessage(messages.errorSavingMessage)}
</Alert>
)}
<FormSwitchGroup
id="enable-flexible-peer-grade"
name="enableFlexiblePeerGrade"
label={(
<div className="d-flex align-items-center">
{formatMessage(messages.enableFlexPeerGradeLabel)}
{formValues.enableFlexiblePeerGrade && (
<Badge className="ml-2" variant="success" data-testid="enable-badge">
{formatMessage(messages.enabledBadgeLabel)}
</Badge>
)}
</div>
)}
helpText={(
<div>
<p>{formatMessage(messages.enableFlexPeerGradeHelp)}</p>
<span className="py-3">
<Hyperlink
className="text-primary-500 small"
destination={appInfo.documentationLinks?.learnMoreConfiguration}
target="_blank"
rel="noreferrer noopener"
>
{formatMessage(messages.ORASettingsHelpLink)}
</Hyperlink>
</span>
</div>
)}
onChange={handleChange}
checked={formValues.enableFlexiblePeerGrade}
/>
</>
);
case RequestStatus.DENIED:
return <PermissionDeniedAlert />;
case RequestStatus.FAILED:
return <ConnectionErrorAlert />;
default:
return <Loading />;
}
};
return (
<AppSettingsModal
appId={appId}
title={title}
<ModalDialog
title={formatMessage(messages.heading)}
isOpen
onClose={onClose}
initialValues={{ enableFlexiblePeerGrade }}
validationSchema={{ enableFlexiblePeerGrade: Yup.boolean() }}
onSettingsSave={handleSettingsSave}
hideAppToggle
size="lg"
variant={modalVariant}
hasCloseButton={isMobile}
isFullscreenScroll
isFullscreenOnMobile
>
{({ values, handleChange, handleBlur }) => (
<FormSwitchGroup
id="enable-flexible-peer-grade"
name="enableFlexiblePeerGrade"
label={intl.formatMessage(messages.enableFlexPeerGradeLabel)}
helpText={intl.formatMessage(messages.enableFlexPeerGradeHelp)}
onChange={handleChange}
onBlur={handleBlur}
checked={values.enableFlexiblePeerGrade}
/>
)}
</AppSettingsModal>
<Form onSubmit={handleSubmit} data-testid="proctoringForm">
<ModalDialog.Header>
<ModalDialog.Title>
{formatMessage(messages.heading)}
</ModalDialog.Title>
</ModalDialog.Header>
<ModalDialog.Body>
{renderBody()}
</ModalDialog.Body>
<ModalDialog.Footer className="p-4">
<ActionRow>
<ModalDialog.CloseButton variant="tertiary">
{formatMessage(messages.cancelLabel)}
</ModalDialog.CloseButton>
<StatefulButton
labels={{
default: formatMessage(messages.saveLabel),
pending: formatMessage(messages.pendingSaveLabel),
}}
description="Form save button"
data-testid="submissionButton"
disabled={submitButtonState === RequestStatus.IN_PROGRESS}
state={submitButtonState}
type="submit"
/>
</ActionRow>
</ModalDialog.Footer>
</Form>
</ModalDialog>
);
};
ORASettings.propTypes = {
intl: intlShape.isRequired,
onClose: PropTypes.func.isRequired,
};
export default injectIntl(ORASettings);
export default ORASettings;

View File

@@ -1,33 +1,152 @@
import { shallow } from '@edx/react-unit-test-utils';
import {
render,
screen,
waitFor,
within,
} from '@testing-library/react';
import ReactDOM from 'react-dom';
import { Routes, Route, MemoryRouter } from 'react-router-dom';
import { initializeMockApp } from '@edx/frontend-platform';
import MockAdapter from 'axios-mock-adapter';
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 'CourseAuthoring/store';
import { executeThunk } from 'CourseAuthoring/utils';
import PagesAndResourcesProvider from 'CourseAuthoring/pages-and-resources/PagesAndResourcesProvider';
import { getCourseAppsApiUrl, getCourseAdvancedSettingsApiUrl } from 'CourseAuthoring/pages-and-resources/data/api';
import { fetchCourseApps, fetchCourseAppSettings } from 'CourseAuthoring/pages-and-resources/data/thunks';
import ORASettings from './Settings';
import messages from './messages';
import {
courseId,
inititalState,
} from './factories/mockData';
jest.mock('@edx/frontend-platform/i18n', () => ({
...jest.requireActual('@edx/frontend-platform/i18n'), // use actual for all non-hook parts
injectIntl: (component) => component,
intlShape: {},
}));
jest.mock('yup', () => ({
boolean: jest.fn().mockReturnValue('Yub.boolean'),
}));
jest.mock('CourseAuthoring/generic/model-store', () => ({
useModel: jest.fn().mockReturnValue({ documentationLinks: { learnMoreConfiguration: 'https://learnmore.test' } }),
}));
jest.mock('CourseAuthoring/generic/FormSwitchGroup', () => 'FormSwitchGroup');
jest.mock('CourseAuthoring/utils', () => ({
useAppSetting: jest.fn().mockReturnValue(['abitrary value', jest.fn().mockName('saveSetting')]),
}));
jest.mock('CourseAuthoring/pages-and-resources/app-settings-modal/AppSettingsModal', () => 'AppSettingsModal');
let axiosMock;
let store;
const oraSettingsUrl = `/course/${courseId}/pages-and-resources/live/settings`;
const props = {
onClose: jest.fn().mockName('onClose'),
intl: {
formatMessage: (message) => message.defaultMessage,
},
// Modal creates a portal. Overriding ReactDOM.createPortal allows portals to be tested in jest.
ReactDOM.createPortal = jest.fn(node => node);
const renderComponent = () => (
render(
<IntlProvider locale="en">
<AppProvider store={store} wrapWithRouter={false}>
<PagesAndResourcesProvider courseId={courseId}>
<MemoryRouter initialEntries={[oraSettingsUrl]}>
<Routes>
<Route path={oraSettingsUrl} element={<PageWrap><ORASettings onClose={jest.fn()} /></PageWrap>} />
</Routes>
</MemoryRouter>
</PagesAndResourcesProvider>
</AppProvider>
</IntlProvider>,
)
);
const mockStore = async ({
apiStatus,
enabled,
}) => {
const settings = ['forceOnFlexiblePeerOpenassessments'];
const fetchCourseAppsUrl = `${getCourseAppsApiUrl()}/${courseId}`;
const fetchAdvancedSettingsUrl = `${getCourseAdvancedSettingsApiUrl()}/${courseId}`;
axiosMock.onGet(fetchCourseAppsUrl).reply(
200,
[{
allowed_operations: { enable: false, configure: true },
description: 'setting',
documentation_links: { learnMoreConfiguration: '' },
enabled,
id: 'ora_settings',
name: 'Flexible Peer Grading for ORAs',
}],
);
axiosMock.onGet(fetchAdvancedSettingsUrl).reply(
apiStatus,
{ force_on_flexible_peer_openassessments: { value: enabled } },
);
await executeThunk(fetchCourseApps(courseId), store.dispatch);
await executeThunk(fetchCourseAppSettings(courseId, settings), store.dispatch);
};
describe('ORASettings', () => {
it('should render', () => {
const wrapper = shallow(<ORASettings {...props} />);
expect(wrapper.snapshot).toMatchSnapshot();
beforeEach(async () => {
initializeMockApp({
authenticatedUser: {
userId: 3,
username: 'abc123',
administrator: false,
roles: [],
},
});
store = initializeStore(inititalState);
axiosMock = new MockAdapter(getAuthenticatedHttpClient());
});
it('Flexible peer grading configuration modal is visible', async () => {
renderComponent();
expect(screen.getByRole('dialog')).toBeVisible();
});
it('Displays "Configure Flexible Peer Grading" heading', async () => {
renderComponent();
const headingElement = screen.getByText(messages.heading.defaultMessage);
expect(headingElement).toBeVisible();
});
it('Displays loading component', () => {
renderComponent();
const loadingElement = screen.getByRole('status');
expect(within(loadingElement).getByText('Loading...')).toBeInTheDocument();
});
it('Displays Connection Error Alert', async () => {
await mockStore({ apiStatus: 404, enabled: true });
renderComponent();
const errorAlert = screen.getByRole('alert');
expect(within(errorAlert).getByText('We encountered a technical error when loading this page.', { exact: false })).toBeVisible();
});
it('Displays Permissions Error Alert', async () => {
await mockStore({ apiStatus: 403, enabled: true });
renderComponent();
const errorAlert = screen.getByRole('alert');
expect(within(errorAlert).getByText('You are not authorized to view this page', { exact: false })).toBeVisible();
});
it('Displays title, helper text and badge when flexible peer grading button is enabled', async () => {
renderComponent();
await mockStore({ apiStatus: 200, enabled: true });
waitFor(() => {
const label = screen.getByText(messages.enableFlexPeerGradeLabel.defaultMessage);
const enableBadge = screen.getByTestId('enable-badge');
expect(label).toBeVisible();
expect(enableBadge).toHaveTextContent('Enabled');
});
});
it('Displays title, helper text and hides badge when flexible peer grading button is disabled', async () => {
renderComponent();
await mockStore({ apiStatus: 200, enabled: false });
const label = screen.getByText(messages.enableFlexPeerGradeLabel.defaultMessage);
const enableBadge = screen.queryByTestId('enable-badge');
expect(label).toBeVisible();
expect(enableBadge).toBeNull();
});
});

View File

@@ -1,41 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`ORASettings should render 1`] = `
<AppSettingsModal
appId="ora_settings"
hideAppToggle={true}
initialValues={
Object {
"enableFlexiblePeerGrade": "abitrary value",
}
}
onClose={[MockFunction onClose]}
onSettingsSave={[Function]}
title={
<div>
<p>
Configure open response assessment
</p>
<div
className="pt-3"
>
<withDeprecatedProps(Hyperlink)
className="text-primary-500 small"
destination="https://learnmore.test"
rel="noreferrer noopener"
target="_blank"
>
Learn more about open response assessment settings
</withDeprecatedProps(Hyperlink)>
</div>
</div>
}
validationSchema={
Object {
"enableFlexiblePeerGrade": "Yub.boolean",
}
}
>
[Function]
</AppSettingsModal>
`;

View File

@@ -0,0 +1,32 @@
export const courseId = 'course-v1:org+num+run';
export const inititalState = {
courseDetail: {
courseId,
status: 'successful',
},
pagesAndResources: {
courseAppIds: ['ora_settings'],
loadingStatus: 'in-progress',
savingStatus: '',
courseAppsApiStatus: {},
courseAppSettings: {},
},
models: {
courseApps: {
ora_settings: {
id: 'ora_settings',
name: 'Flexible Peer Grading',
enabled: true,
description: 'Enable flexible peer grading',
allowedOperations: {
enable: false,
configure: true,
},
documentationLinks: {
learnMoreConfiguration: '',
},
},
},
},
};

View File

@@ -3,19 +3,51 @@ import { defineMessages } from '@edx/frontend-platform/i18n';
const messages = defineMessages({
heading: {
id: 'course-authoring.pages-resources.ora.heading',
defaultMessage: 'Configure open response assessment',
defaultMessage: 'Configure Flexible Peer Grading',
description: 'Title for the modal dialog header',
},
ORASettingsHelpLink: {
id: 'course-authoring.pages-resources.ora.flex-peer-grading.link',
defaultMessage: 'Learn more about open response assessment settings',
description: 'Descriptive text for the hyperlink to the docs site',
},
enableFlexPeerGradeLabel: {
id: 'course-authoring.pages-resources.ora.flex-peer-grading.label',
defaultMessage: 'Flex Peer Grading',
description: 'Label for form switch',
},
enableFlexPeerGradeHelp: {
id: 'course-authoring.pages-resources.ora.flex-peer-grading.help',
defaultMessage: 'Turn on Flexible Peer Grading for all open response assessments in the course with peer grading.',
description: 'Help text describing what happens when the switch is enabled',
},
enabledBadgeLabel: {
id: 'course-authoring.pages-resources.ora.flex-peer-grading.enabled-badge.label',
defaultMessage: 'Enabled',
description: 'Label for badge that show users that a setting is enabled',
},
cancelLabel: {
id: 'course-authoring.pages-resources.ora.flex-peer-grading.cancel-button.label',
defaultMessage: 'Cancel',
description: 'Label for button that cancels user changes',
},
saveLabel: {
id: 'course-authoring.pages-resources.ora.flex-peer-grading.save-button.label',
defaultMessage: 'Save',
description: 'Label for button that saves user changes',
},
pendingSaveLabel: {
id: 'course-authoring.pages-resources.ora.flex-peer-grading.pending-save-button.label',
defaultMessage: 'Saving',
description: 'Label for button that has pending api save calls',
},
errorSavingTitle: {
id: 'course-authoring.pages-resources.ora.flex-peer-grading.save-error.title',
defaultMessage: 'We couldn\'t apply your changes.',
},
errorSavingMessage: {
id: 'course-authoring.pages-resources.ora.flex-peer-grading.save-error.message',
defaultMessage: 'Please check your entries and try again.',
},
});

View File

@@ -3,15 +3,16 @@
"version": "0.1.0",
"description": "Open Response Assessment configuration for courses using it",
"peerDependencies": {
"@edx/frontend-app-course-authoring": "*",
"@edx/frontend-app-authoring": "*",
"@edx/frontend-platform": "*",
"@openedx/paragon": "*",
"prop-types": "*",
"react": "*",
"react-redux": "*",
"yup": "*"
},
"peerDependenciesMeta": {
"@edx/frontend-app-course-authoring": {
"@edx/frontend-app-authoring": {
"optional": true
}
}

View File

@@ -64,6 +64,8 @@ const ProctoringSettings = ({ intl, onClose }) => {
}
const { courseId } = useContext(PagesAndResourcesContext);
const courseDetails = useModel('courseDetails', courseId);
const org = courseDetails?.org;
const appInfo = useModel('courseApps', 'proctoring');
const alertRef = React.createRef();
const saveStatusAlertRef = React.createRef();
@@ -146,9 +148,9 @@ const ProctoringSettings = ({ intl, onClose }) => {
setSaveSuccess(true);
setSaveError(false);
setSubmissionInProgress(false);
}).catch(() => {
}).catch((error) => {
setSaveSuccess(false);
setSaveError(true);
setSaveError(error);
setSubmissionInProgress(false);
});
}
@@ -458,21 +460,32 @@ const ProctoringSettings = ({ intl, onClose }) => {
}
function renderSaveError() {
return (
<Alert
variant="danger"
data-testid="saveError"
tabIndex="-1"
ref={saveStatusAlertRef}
onClose={() => setSaveError(false)}
dismissible
>
let errorMessage = (
<FormattedMessage
id="authoring.proctoring.alert.error"
defaultMessage={`
We encountered a technical error while trying to save proctored exam settings.
This might be a temporary issue, so please try again in a few minutes.
If the problem persists, please go to the {support_link} for help.
`}
values={{
support_link: (
<Alert.Link href={getConfig().SUPPORT_URL}>
{intl.formatMessage(messages['authoring.proctoring.support.text'])}
</Alert.Link>
),
}}
/>
);
if (saveError?.response.status === 403) {
errorMessage = (
<FormattedMessage
id="authoring.examsettings.alert.error"
id="authoring.proctoring.alert.error.forbidden"
defaultMessage={`
We encountered a technical error while trying to save proctored exam settings.
This might be a temporary issue, so please try again in a few minutes.
If the problem persists, please go to the {support_link} for help.
You do not have permission to edit proctored exam settings for this course.
If you are a course team member and this problem persists,
please go to the {support_link} for help.
`}
values={{
support_link: (
@@ -482,6 +495,19 @@ const ProctoringSettings = ({ intl, onClose }) => {
),
}}
/>
);
}
return (
<Alert
variant="danger"
data-testid="saveError"
tabIndex="-1"
ref={saveStatusAlertRef}
onClose={() => setSaveError(false)}
dismissible
>
{errorMessage}
</Alert>
);
}
@@ -490,7 +516,7 @@ const ProctoringSettings = ({ intl, onClose }) => {
Promise.all([
StudioApiService.getProctoredExamSettingsData(courseId),
ExamsApiService.isAvailable() ? ExamsApiService.getCourseExamConfiguration(courseId) : Promise.resolve(),
ExamsApiService.isAvailable() ? ExamsApiService.getAvailableProviders() : Promise.resolve(),
ExamsApiService.isAvailable() ? ExamsApiService.getAvailableProviders(org) : Promise.resolve(),
])
.then(
([settingsResponse, examConfigResponse, ltiProvidersResponse]) => {

View File

@@ -15,8 +15,9 @@ import initializeStore from 'CourseAuthoring/store';
import PagesAndResourcesProvider from 'CourseAuthoring/pages-and-resources/PagesAndResourcesProvider';
import ProctoredExamSettings from './Settings';
const courseId = 'course-v1%3AedX%2BDemoX%2BDemo_Course';
const defaultProps = {
courseId: 'course-v1%3AedX%2BDemoX%2BDemo_Course',
courseId,
onClose: () => {},
};
const IntlProctoredExamSettings = injectIntl(ProctoredExamSettings);
@@ -34,7 +35,7 @@ const intlWrapper = children => (
let axiosMock;
describe('ProctoredExamSettings', () => {
function setupApp(isAdmin = true) {
function setupApp(isAdmin = true, org = undefined) {
mergeConfig({
EXAMS_BASE_URL: 'http://exams.testing.co',
}, 'CourseAuthoringConfig');
@@ -52,12 +53,18 @@ describe('ProctoredExamSettings', () => {
courseApps: {
proctoring: {},
},
courseDetails: {
[courseId]: {
start: Date(),
},
},
...(org ? { courseDetails: { [courseId]: { org } } } : {}),
},
});
axiosMock = new MockAdapter(getAuthenticatedHttpClient());
axiosMock.onGet(
`${ExamsApiService.getExamsBaseUrl()}/api/v1/providers`,
`${ExamsApiService.getExamsBaseUrl()}/api/v1/providers${org ? `?org=${org}` : ''}`,
).reply(200, [
{
name: 'test_lti',
@@ -103,9 +110,7 @@ describe('ProctoredExamSettings', () => {
screen.getByDisplayValue('mockproc');
});
const selectElement = screen.getByDisplayValue('mockproc');
await act(async () => {
fireEvent.change(selectElement, { target: { value: 'proctortrack' } });
});
fireEvent.change(selectElement, { target: { value: 'proctortrack' } });
const zendeskTicketInput = screen.getByTestId('createZendeskTicketsNo');
expect(zendeskTicketInput.checked).toEqual(true);
});
@@ -115,9 +120,7 @@ describe('ProctoredExamSettings', () => {
screen.getByDisplayValue('mockproc');
});
const selectElement = screen.getByDisplayValue('mockproc');
await act(async () => {
fireEvent.change(selectElement, { target: { value: 'software_secure' } });
});
fireEvent.change(selectElement, { target: { value: 'software_secure' } });
const zendeskTicketInput = screen.getByTestId('createZendeskTicketsYes');
expect(zendeskTicketInput.checked).toEqual(true);
});
@@ -127,9 +130,7 @@ describe('ProctoredExamSettings', () => {
screen.getByDisplayValue('mockproc');
});
const selectElement = screen.getByDisplayValue('mockproc');
await act(async () => {
fireEvent.change(selectElement, { target: { value: 'mockproc' } });
});
fireEvent.change(selectElement, { target: { value: 'mockproc' } });
const zendeskTicketInput = screen.getByTestId('createZendeskTicketsYes');
expect(zendeskTicketInput.checked).toEqual(true);
});
@@ -176,9 +177,7 @@ describe('ProctoredExamSettings', () => {
let enabledProctoredExamCheck = screen.getAllByLabelText('Proctored exams', { exact: false })[0];
expect(enabledProctoredExamCheck.checked).toEqual(true);
await act(async () => {
fireEvent.click(enabledProctoredExamCheck, { target: { value: false } });
});
fireEvent.click(enabledProctoredExamCheck, { target: { value: false } });
enabledProctoredExamCheck = screen.getByLabelText('Proctored exams');
expect(enabledProctoredExamCheck.checked).toEqual(false);
expect(screen.queryByText('Allow opting out of proctored exams')).toBeNull();
@@ -193,9 +192,7 @@ describe('ProctoredExamSettings', () => {
screen.getByDisplayValue('mockproc');
});
const selectElement = screen.getByDisplayValue('mockproc');
await act(async () => {
fireEvent.change(selectElement, { target: { value: 'test_lti' } });
});
fireEvent.change(selectElement, { target: { value: 'test_lti' } });
expect(screen.queryByTestId('allowOptingOutRadio')).toBeNull();
expect(screen.queryByTestId('createZendeskTicketsYes')).toBeNull();
expect(screen.queryByTestId('createZendeskTicketsNo')).toBeNull();
@@ -237,13 +234,9 @@ describe('ProctoredExamSettings', () => {
screen.getByDisplayValue('proctortrack');
});
const selectEscalationEmailElement = screen.getByDisplayValue('test@example.com');
await act(async () => {
fireEvent.change(selectEscalationEmailElement, { target: { value: '' } });
});
fireEvent.change(selectEscalationEmailElement, { target: { value: '' } });
const selectButton = screen.getByTestId('submissionButton');
await act(async () => {
fireEvent.click(selectButton);
});
fireEvent.click(selectButton);
// verify alert content and focus management
const escalationEmailError = screen.getByTestId('escalationEmailError');
@@ -252,9 +245,7 @@ describe('ProctoredExamSettings', () => {
// verify alert link links to offending input
const errorLink = screen.getByTestId('escalationEmailErrorLink');
await act(async () => {
fireEvent.click(errorLink);
});
fireEvent.click(errorLink);
const escalationEmailInput = screen.getByTestId('escalationEmail');
expect(document.activeElement).toEqual(escalationEmailInput);
});
@@ -265,18 +256,12 @@ describe('ProctoredExamSettings', () => {
});
const selectElement = screen.getByDisplayValue('proctortrack');
await act(async () => {
fireEvent.change(selectElement, { target: { value: provider } });
});
fireEvent.change(selectElement, { target: { value: provider } });
const selectEscalationEmailElement = screen.getByDisplayValue('test@example.com');
await act(async () => {
fireEvent.change(selectEscalationEmailElement, { target: { value: 'foo.bar' } });
});
const selectButton = screen.getByTestId('submissionButton');
await act(async () => {
fireEvent.click(selectButton);
});
fireEvent.change(selectEscalationEmailElement, { target: { value: 'foo.bar' } });
const proctoringForm = screen.getByTestId('proctoringForm');
fireEvent.submit(proctoringForm);
// verify alert content and focus management
const escalationEmailError = screen.getByTestId('escalationEmailError');
@@ -286,9 +271,7 @@ describe('ProctoredExamSettings', () => {
// verify alert link links to offending input
const errorLink = screen.getByTestId('escalationEmailErrorLink');
await act(async () => {
fireEvent.click(errorLink);
});
fireEvent.click(errorLink);
const escalationEmailInput = screen.getByTestId('escalationEmail');
expect(document.activeElement).toEqual(escalationEmailInput);
});
@@ -298,15 +281,11 @@ describe('ProctoredExamSettings', () => {
screen.getByDisplayValue('proctortrack');
});
const selectEscalationEmailElement = screen.getByDisplayValue('test@example.com');
await act(async () => {
fireEvent.change(selectEscalationEmailElement, { target: { value: 'foo.bar' } });
});
fireEvent.change(selectEscalationEmailElement, { target: { value: 'foo.bar' } });
const enableProctoringElement = screen.getByText('Proctored exams');
await act(async () => fireEvent.click(enableProctoringElement));
fireEvent.click(enableProctoringElement);
const selectButton = screen.getByTestId('submissionButton');
await act(async () => {
fireEvent.click(selectButton);
});
fireEvent.click(selectButton);
// verify alert content and focus management
const escalationEmailError = screen.getByTestId('escalationEmailError');
@@ -320,24 +299,22 @@ describe('ProctoredExamSettings', () => {
screen.getByDisplayValue('proctortrack');
});
const selectEscalationEmailElement = screen.getByDisplayValue('test@example.com');
await act(async () => {
fireEvent.change(selectEscalationEmailElement, { target: { value: '' } });
});
fireEvent.change(selectEscalationEmailElement, { target: { value: '' } });
const enableProctoringElement = screen.getByText('Proctored exams');
await act(async () => fireEvent.click(enableProctoringElement));
fireEvent.click(enableProctoringElement);
const selectButton = screen.getByTestId('submissionButton');
await act(async () => {
fireEvent.click(selectButton);
});
fireEvent.click(selectButton);
// verify there is no escalation email alert, and focus has been set on save success alert
expect(screen.queryByTestId('escalationEmailError')).toBeNull();
const errorAlert = screen.getByTestId('saveSuccess');
expect(errorAlert.textContent).toEqual(
expect.stringContaining('Proctored exam settings saved successfully.'),
);
expect(document.activeElement).toEqual(errorAlert);
await waitFor(() => {
const errorAlert = screen.getByTestId('saveSuccess');
expect(errorAlert.textContent).toEqual(
expect.stringContaining('Proctored exam settings saved successfully.'),
);
expect(document.activeElement).toEqual(errorAlert);
});
});
it(`Has no error when valid proctoring escalation email is provided with ${provider} selected`, async () => {
@@ -345,22 +322,20 @@ describe('ProctoredExamSettings', () => {
screen.getByDisplayValue('proctortrack');
});
const selectEscalationEmailElement = screen.getByDisplayValue('test@example.com');
await act(async () => {
fireEvent.change(selectEscalationEmailElement, { target: { value: 'foo@bar.com' } });
});
fireEvent.change(selectEscalationEmailElement, { target: { value: 'foo@bar.com' } });
const selectButton = screen.getByTestId('submissionButton');
await act(async () => {
fireEvent.click(selectButton);
});
fireEvent.click(selectButton);
// verify there is no escalation email alert, and focus has been set on save success alert
expect(screen.queryByTestId('escalationEmailError')).toBeNull();
const errorAlert = screen.getByTestId('saveSuccess');
expect(errorAlert.textContent).toEqual(
expect.stringContaining('Proctored exam settings saved successfully.'),
);
expect(document.activeElement).toEqual(errorAlert);
await waitFor(() => {
const errorAlert = screen.getByTestId('saveSuccess');
expect(errorAlert.textContent).toEqual(
expect.stringContaining('Proctored exam settings saved successfully.'),
);
expect(document.activeElement).toEqual(errorAlert);
});
});
it(`Escalation email field hidden when proctoring backend is not ${provider}`, async () => {
@@ -370,9 +345,7 @@ describe('ProctoredExamSettings', () => {
const proctoringBackendSelect = screen.getByDisplayValue('proctortrack');
const selectEscalationEmailElement = screen.getByTestId('escalationEmail');
expect(selectEscalationEmailElement.value).toEqual('test@example.com');
await act(async () => {
fireEvent.change(proctoringBackendSelect, { target: { value: 'software_secure' } });
});
fireEvent.change(proctoringBackendSelect, { target: { value: 'software_secure' } });
expect(screen.queryByTestId('escalationEmail')).toBeNull();
});
@@ -382,13 +355,9 @@ describe('ProctoredExamSettings', () => {
});
const proctoringBackendSelect = screen.getByDisplayValue('proctortrack');
let selectEscalationEmailElement = screen.getByTestId('escalationEmail');
await act(async () => {
fireEvent.change(proctoringBackendSelect, { target: { value: 'software_secure' } });
});
fireEvent.change(proctoringBackendSelect, { target: { value: 'software_secure' } });
expect(screen.queryByTestId('escalationEmail')).toBeNull();
await act(async () => {
fireEvent.change(proctoringBackendSelect, { target: { value: 'proctortrack' } });
});
fireEvent.change(proctoringBackendSelect, { target: { value: 'proctortrack' } });
expect(screen.queryByTestId('escalationEmail')).toBeDefined();
selectEscalationEmailElement = screen.getByTestId('escalationEmail');
expect(selectEscalationEmailElement.value).toEqual('test@example.com');
@@ -399,12 +368,8 @@ describe('ProctoredExamSettings', () => {
screen.getByDisplayValue('proctortrack');
});
const selectEscalationEmailElement = screen.getByDisplayValue('test@example.com');
await act(async () => {
fireEvent.change(selectEscalationEmailElement, { target: { value: '' } });
});
await act(async () => {
fireEvent.submit(selectEscalationEmailElement);
});
fireEvent.change(selectEscalationEmailElement, { target: { value: '' } });
fireEvent.submit(selectEscalationEmailElement);
// if the error appears, the form has been submitted
expect(screen.getByTestId('escalationEmailError')).toBeDefined();
});
@@ -458,6 +423,16 @@ describe('ProctoredExamSettings', () => {
expect(providerOption.hasAttribute('disabled')).toEqual(false);
});
it('Sends the org to the proctoring provider endpoint', async () => {
const isAdmin = false;
const org = 'test-org';
setupApp(isAdmin, org);
mockCourseData(mockGetFutureCourseData);
await act(async () => render(intlWrapper(<IntlProctoredExamSettings {...defaultProps} />)));
const providerOption = screen.getByTestId('proctortrack');
expect(providerOption.hasAttribute('disabled')).toEqual(false);
});
it('Enables all proctoring provider options if user administrator and it is after start date', async () => {
const isAdmin = true;
setupApp(isAdmin);
@@ -628,9 +603,7 @@ describe('ProctoredExamSettings', () => {
await act(async () => render(intlWrapper(<IntlProctoredExamSettings {...defaultProps} />)));
let submitButton = screen.getByTestId('submissionButton');
expect(screen.queryByTestId('saveInProgress')).toBeFalsy();
act(() => {
fireEvent.click(submitButton);
});
fireEvent.click(submitButton);
submitButton = screen.getByTestId('submissionButton');
expect(submitButton).toHaveAttribute('disabled');
@@ -640,19 +613,13 @@ describe('ProctoredExamSettings', () => {
await act(async () => render(intlWrapper(<IntlProctoredExamSettings {...defaultProps} />)));
// Make a change to the provider to proctortrack and set the email
const selectElement = screen.getByDisplayValue('mockproc');
await act(async () => {
fireEvent.change(selectElement, { target: { value: 'proctortrack' } });
});
fireEvent.change(selectElement, { target: { value: 'proctortrack' } });
const escalationEmail = screen.getByTestId('escalationEmail');
expect(escalationEmail.value).toEqual('test@example.com');
await act(async () => {
fireEvent.change(escalationEmail, { target: { value: 'proctortrack@example.com' } });
});
fireEvent.change(escalationEmail, { target: { value: 'proctortrack@example.com' } });
expect(escalationEmail.value).toEqual('proctortrack@example.com');
const submitButton = screen.getByTestId('submissionButton');
await act(async () => {
fireEvent.click(submitButton);
});
fireEvent.click(submitButton);
expect(axiosMock.history.post.length).toBe(1);
expect(JSON.parse(axiosMock.history.post[0].data)).toEqual({
proctored_exam_settings: {
@@ -664,11 +631,13 @@ describe('ProctoredExamSettings', () => {
},
});
const errorAlert = screen.getByTestId('saveSuccess');
expect(errorAlert.textContent).toEqual(
expect.stringContaining('Proctored exam settings saved successfully.'),
);
expect(document.activeElement).toEqual(errorAlert);
await waitFor(() => {
const errorAlert = screen.getByTestId('saveSuccess');
expect(errorAlert.textContent).toEqual(
expect.stringContaining('Proctored exam settings saved successfully.'),
);
expect(document.activeElement).toEqual(errorAlert);
});
});
it('Makes API call successfully without proctoring_escalation_email if not proctortrack', async () => {
@@ -678,9 +647,7 @@ describe('ProctoredExamSettings', () => {
expect(screen.getByDisplayValue('mockproc')).toBeDefined();
const submitButton = screen.getByTestId('submissionButton');
await act(async () => {
fireEvent.click(submitButton);
});
fireEvent.click(submitButton);
expect(axiosMock.history.post.length).toBe(1);
expect(JSON.parse(axiosMock.history.post[0].data)).toEqual({
proctored_exam_settings: {
@@ -691,32 +658,28 @@ describe('ProctoredExamSettings', () => {
},
});
const errorAlert = screen.getByTestId('saveSuccess');
expect(errorAlert.textContent).toEqual(
expect.stringContaining('Proctored exam settings saved successfully.'),
);
expect(document.activeElement).toEqual(errorAlert);
await waitFor(() => {
const errorAlert = screen.getByTestId('saveSuccess');
expect(errorAlert.textContent).toEqual(
expect.stringContaining('Proctored exam settings saved successfully.'),
);
expect(document.activeElement).toEqual(errorAlert);
});
});
it('Successfully updates exam configuration and studio provider is set to "lti_external" for lti providers', async () => {
await act(async () => render(intlWrapper(<IntlProctoredExamSettings {...defaultProps} />)));
// Make a change to the provider to test_lti and set the email
const selectElement = screen.getByDisplayValue('mockproc');
await act(async () => {
fireEvent.change(selectElement, { target: { value: 'test_lti' } });
});
fireEvent.change(selectElement, { target: { value: 'test_lti' } });
const escalationEmail = screen.getByTestId('escalationEmail');
expect(escalationEmail.value).toEqual('test@example.com');
await act(async () => {
fireEvent.change(escalationEmail, { target: { value: 'test_lti@example.com' } });
});
fireEvent.change(escalationEmail, { target: { value: 'test_lti@example.com' } });
expect(escalationEmail.value).toEqual('test_lti@example.com');
const submitButton = screen.getByTestId('submissionButton');
await act(async () => {
fireEvent.click(submitButton);
});
fireEvent.click(submitButton);
// update exam service config
expect(axiosMock.history.patch.length).toBe(1);
@@ -736,19 +699,19 @@ describe('ProctoredExamSettings', () => {
},
});
const errorAlert = screen.getByTestId('saveSuccess');
expect(errorAlert.textContent).toEqual(
expect.stringContaining('Proctored exam settings saved successfully.'),
);
expect(document.activeElement).toEqual(errorAlert);
await waitFor(() => {
const errorAlert = screen.getByTestId('saveSuccess');
expect(errorAlert.textContent).toEqual(
expect.stringContaining('Proctored exam settings saved successfully.'),
);
expect(document.activeElement).toEqual(errorAlert);
});
});
it('Sets exam service provider to null if a non-lti provider is selected', async () => {
await act(async () => render(intlWrapper(<IntlProctoredExamSettings {...defaultProps} />)));
const submitButton = screen.getByTestId('submissionButton');
await act(async () => {
fireEvent.click(submitButton);
});
fireEvent.click(submitButton);
// update exam service config
expect(axiosMock.history.patch.length).toBe(1);
expect(JSON.parse(axiosMock.history.patch[0].data)).toEqual({
@@ -766,11 +729,13 @@ describe('ProctoredExamSettings', () => {
},
});
const errorAlert = screen.getByTestId('saveSuccess');
expect(errorAlert.textContent).toEqual(
expect.stringContaining('Proctored exam settings saved successfully.'),
);
expect(document.activeElement).toEqual(errorAlert);
await waitFor(() => {
const errorAlert = screen.getByTestId('saveSuccess');
expect(errorAlert.textContent).toEqual(
expect.stringContaining('Proctored exam settings saved successfully.'),
);
expect(document.activeElement).toEqual(errorAlert);
});
});
it('Does not update exam service if lti is not enabled in studio', async () => {
@@ -790,9 +755,7 @@ describe('ProctoredExamSettings', () => {
await act(async () => render(intlWrapper(<IntlProctoredExamSettings {...defaultProps} />)));
const submitButton = screen.getByTestId('submissionButton');
await act(async () => {
fireEvent.click(submitButton);
});
fireEvent.click(submitButton);
// does not update exam service config
expect(axiosMock.history.patch.length).toBe(0);
// does update studio
@@ -806,11 +769,13 @@ describe('ProctoredExamSettings', () => {
},
});
const errorAlert = screen.getByTestId('saveSuccess');
expect(errorAlert.textContent).toEqual(
expect.stringContaining('Proctored exam settings saved successfully.'),
);
expect(document.activeElement).toEqual(errorAlert);
await waitFor(() => {
const errorAlert = screen.getByTestId('saveSuccess');
expect(errorAlert.textContent).toEqual(
expect.stringContaining('Proctored exam settings saved successfully.'),
);
expect(document.activeElement).toEqual(errorAlert);
});
});
it('Makes studio API call generated error', async () => {
@@ -820,15 +785,15 @@ describe('ProctoredExamSettings', () => {
await act(async () => render(intlWrapper(<IntlProctoredExamSettings {...defaultProps} />)));
const submitButton = screen.getByTestId('submissionButton');
await act(async () => {
fireEvent.click(submitButton);
});
fireEvent.click(submitButton);
expect(axiosMock.history.post.length).toBe(1);
const errorAlert = screen.getByTestId('saveError');
expect(errorAlert.textContent).toEqual(
expect.stringContaining('We encountered a technical error while trying to save proctored exam settings'),
);
expect(document.activeElement).toEqual(errorAlert);
await waitFor(() => {
const errorAlert = screen.getByTestId('saveError');
expect(errorAlert.textContent).toEqual(
expect.stringContaining('We encountered a technical error while trying to save proctored exam settings'),
);
expect(document.activeElement).toEqual(errorAlert);
});
});
it('Makes exams API call generated error', async () => {
@@ -838,15 +803,33 @@ describe('ProctoredExamSettings', () => {
await act(async () => render(intlWrapper(<IntlProctoredExamSettings {...defaultProps} />)));
const submitButton = screen.getByTestId('submissionButton');
await act(async () => {
fireEvent.click(submitButton);
});
fireEvent.click(submitButton);
expect(axiosMock.history.post.length).toBe(1);
const errorAlert = screen.getByTestId('saveError');
expect(errorAlert.textContent).toEqual(
expect.stringContaining('We encountered a technical error while trying to save proctored exam settings'),
);
expect(document.activeElement).toEqual(errorAlert);
await waitFor(() => {
const errorAlert = screen.getByTestId('saveError');
expect(errorAlert.textContent).toEqual(
expect.stringContaining('We encountered a technical error while trying to save proctored exam settings'),
);
expect(document.activeElement).toEqual(errorAlert);
});
});
test('Exams API permission error', async () => {
axiosMock.onPatch(
`${ExamsApiService.getExamsBaseUrl()}/api/v1/configs/course_id/${defaultProps.courseId}`,
).reply(403, 'error');
await act(async () => render(intlWrapper(<IntlProctoredExamSettings {...defaultProps} />)));
const submitButton = screen.getByTestId('submissionButton');
fireEvent.click(submitButton);
expect(axiosMock.history.post.length).toBe(1);
await waitFor(() => {
const errorAlert = screen.getByTestId('saveError');
expect(errorAlert.textContent).toEqual(
expect.stringContaining('You do not have permission to edit proctored exam settings for this course'),
);
expect(document.activeElement).toEqual(errorAlert);
});
});
it('Manages focus correctly after different save statuses', async () => {
@@ -857,30 +840,30 @@ describe('ProctoredExamSettings', () => {
await act(async () => render(intlWrapper(<IntlProctoredExamSettings {...defaultProps} />)));
const submitButton = screen.getByTestId('submissionButton');
await act(async () => {
fireEvent.click(submitButton);
});
fireEvent.click(submitButton);
expect(axiosMock.history.post.length).toBe(1);
const errorAlert = screen.getByTestId('saveError');
expect(errorAlert.textContent).toEqual(
expect.stringContaining('We encountered a technical error while trying to save proctored exam settings'),
);
expect(document.activeElement).toEqual(errorAlert);
await waitFor(() => {
const errorAlert = screen.getByTestId('saveError');
expect(errorAlert.textContent).toEqual(
expect.stringContaining('We encountered a technical error while trying to save proctored exam settings'),
);
expect(document.activeElement).toEqual(errorAlert);
});
// now make a call that will allow for a successful save
axiosMock.onPost(
StudioApiService.getProctoredExamSettingsUrl(defaultProps.courseId),
).reply(200, 'success');
await act(async () => {
fireEvent.click(submitButton);
});
fireEvent.click(submitButton);
expect(axiosMock.history.post.length).toBe(2);
const successAlert = screen.getByTestId('saveSuccess');
expect(successAlert.textContent).toEqual(
expect.stringContaining('Proctored exam settings saved successfully.'),
);
expect(document.activeElement).toEqual(successAlert);
await waitFor(() => {
const successAlert = screen.getByTestId('saveSuccess');
expect(successAlert.textContent).toEqual(
expect.stringContaining('Proctored exam settings saved successfully.'),
);
expect(document.activeElement).toEqual(successAlert);
});
});
it('Include Zendesk ticket in post request if user is not an admin', async () => {
@@ -891,13 +874,9 @@ describe('ProctoredExamSettings', () => {
await act(async () => render(intlWrapper(<IntlProctoredExamSettings {...defaultProps} />)));
// Make a change to the proctoring provider
const selectElement = screen.getByDisplayValue('mockproc');
await act(async () => {
fireEvent.change(selectElement, { target: { value: 'proctortrack' } });
});
fireEvent.change(selectElement, { target: { value: 'proctortrack' } });
const submitButton = screen.getByTestId('submissionButton');
await act(async () => {
fireEvent.click(submitButton);
});
fireEvent.click(submitButton);
expect(axiosMock.history.post.length).toBe(1);
expect(JSON.parse(axiosMock.history.post[0].data)).toEqual({
proctored_exam_settings: {

View File

@@ -1,6 +1,16 @@
import { defineMessages } from '@edx/frontend-platform/i18n';
const messages = defineMessages({
'authoring.proctoring.alert.error': {
id: 'authoring.proctoring.alert.error',
defaultMessage: 'We encountered a technical error while trying to save proctored exam settings. This might be a temporary issue, so please try again in a few minutes. If the problem persists, please go to the {support_link} for help.',
description: 'Alert message for proctoring settings save error.',
},
'authoring.proctoring.alert.forbidden': {
id: 'authoring.proctoring.alert.forbidden',
defaultMessage: 'You do not have permission to edit proctored exam settings for this course. If you are a course team member and this problem persists, please go to the {support_link} for help.',
description: 'Alert message for proctoring settings permission error.',
},
'authoring.proctoring.no': {
id: 'authoring.proctoring.no',
defaultMessage: 'No',

View File

@@ -3,7 +3,7 @@
"version": "0.1.0",
"description": "Proctoring configuration for courses using it",
"peerDependencies": {
"@edx/frontend-app-course-authoring": "*",
"@edx/frontend-app-authoring": "*",
"@edx/frontend-platform": "*",
"@openedx/paragon": "*",
"classnames": "*",
@@ -13,7 +13,7 @@
"moment": "*"
},
"peerDependenciesMeta": {
"@edx/frontend-app-course-authoring": {
"@edx/frontend-app-authoring": {
"optional": true
}
}

View File

@@ -3,7 +3,7 @@
"version": "0.1.0",
"description": "Progress configuration for courses using it",
"peerDependencies": {
"@edx/frontend-app-course-authoring": "*",
"@edx/frontend-app-authoring": "*",
"@edx/frontend-platform": "*",
"@openedx/paragon": "*",
"prop-types": "*",
@@ -11,7 +11,7 @@
"yup": "*"
},
"peerDependenciesMeta": {
"@edx/frontend-app-course-authoring": {
"@edx/frontend-app-authoring": {
"optional": true
}
}

View File

@@ -3,7 +3,7 @@
"version": "0.1.0",
"description": "Teams configuration for courses using it",
"peerDependencies": {
"@edx/frontend-app-course-authoring": "*",
"@edx/frontend-app-authoring": "*",
"@edx/frontend-platform": "*",
"@openedx/paragon": "*",
"formik": "*",
@@ -13,7 +13,7 @@
"yup": "*"
},
"peerDependenciesMeta": {
"@edx/frontend-app-course-authoring": {
"@edx/frontend-app-authoring": {
"optional": true
}
}

View File

@@ -26,8 +26,8 @@ const messages = defineMessages({
},
enablePublicWikiHelp: {
id: 'course-authoring.pages-resources.wiki.enable-public-wiki.help',
defaultMessage: `If enabled, edX users can view the course wiki even when
they're not enrolled in the course.`,
defaultMessage: `If enabled, any registered user can view the course wiki
even if they are not enrolled in the course`,
},
});

View File

@@ -3,7 +3,7 @@
"version": "0.1.0",
"description": "Wiki configuration for courses using it",
"peerDependencies": {
"@edx/frontend-app-course-authoring": "*",
"@edx/frontend-app-authoring": "*",
"@edx/frontend-platform": "*",
"@openedx/paragon": "*",
"prop-types": "*",
@@ -11,7 +11,7 @@
"yup": "*"
},
"peerDependenciesMeta": {
"@edx/frontend-app-course-authoring": {
"@edx/frontend-app-authoring": {
"optional": true
}
}

View File

@@ -3,7 +3,7 @@
"version": "0.1.0",
"description": "Xpert Unit Summaries configuration for courses using it",
"peerDependencies": {
"@edx/frontend-app-course-authoring": "*",
"@edx/frontend-app-authoring": "*",
"@edx/frontend-platform": "*",
"@openedx/paragon": "*",
"formik": "*",
@@ -14,7 +14,7 @@
"react-router-dom": "*"
},
"peerDependenciesMeta": {
"@edx/frontend-app-course-authoring": {
"@edx/frontend-app-authoring": {
"optional": true
}
}

View File

@@ -137,12 +137,12 @@ const ResetUnitsButton = ({
const getResetButtonState = () => {
switch (resetStatusRequestStatus) {
case RequestStatus.PENDING:
return 'pending';
case RequestStatus.SUCCESSFUL:
return 'finish';
default:
return 'default';
case RequestStatus.PENDING:
return 'pending';
case RequestStatus.SUCCESSFUL:
return 'finish';
default:
return 'default';
}
};
@@ -246,7 +246,7 @@ const SettingsModal = ({
success = success && await onSettingsSave(values);
}
setSaveError(!success);
!success && alertRef?.current.scrollIntoView(); // eslint-disable-line no-unused-expressions
!success && alertRef?.current.scrollIntoView(); // eslint-disable-line @typescript-eslint/no-unused-expressions
};
const handleFormikSubmit = ({ handleSubmit, errors }) => async (event) => {

View File

@@ -19,15 +19,6 @@
"matchPackagePatterns": ["@edx", "@openedx"],
"matchUpdateTypes": ["minor", "patch"],
"automerge": false
},
{
"matchPackagePatterns": ["@edx/frontend-lib-content-components"],
"matchUpdateTypes": ["minor", "patch"],
"automerge": false,
"schedule": [
"after 1am",
"before 11pm"
]
}
]
}

View File

@@ -11,33 +11,11 @@ import { fetchCourseDetail } from './data/thunks';
import { useModel } from './generic/model-store';
import NotFoundAlert from './generic/NotFoundAlert';
import PermissionDeniedAlert from './generic/PermissionDeniedAlert';
import { fetchStudioHomeData } from './studio-home/data/thunks';
import { getCourseAppsApiStatus } from './pages-and-resources/data/selectors';
import { RequestStatus } from './data/constants';
import Loading from './generic/Loading';
const AppHeader = ({
courseNumber, courseOrg, courseTitle, courseId,
}) => (
<Header
courseNumber={courseNumber}
courseOrg={courseOrg}
courseTitle={courseTitle}
courseId={courseId}
/>
);
AppHeader.propTypes = {
courseId: PropTypes.string.isRequired,
courseNumber: PropTypes.string,
courseOrg: PropTypes.string,
courseTitle: PropTypes.string.isRequired,
};
AppHeader.defaultProps = {
courseNumber: null,
courseOrg: null,
};
const CourseAuthoringPage = ({ courseId, children }) => {
const dispatch = useDispatch();
@@ -45,6 +23,10 @@ const CourseAuthoringPage = ({ courseId, children }) => {
dispatch(fetchCourseDetail(courseId));
}, [courseId]);
useEffect(() => {
dispatch(fetchStudioHomeData());
}, []);
const courseDetail = useModel('courseDetails', courseId);
const courseNumber = courseDetail ? courseDetail.number : null;
@@ -67,18 +49,18 @@ const CourseAuthoringPage = ({ courseId, children }) => {
);
}
return (
<div className={pathname.includes('/editor/') ? '' : 'bg-light-200'}>
<div>
{/* While V2 Editors are temporarily served from their own pages
using url pattern containing /editor/,
we shouldn't have the header and footer on these pages.
This functionality will be removed in TNL-9591 */}
{inProgress ? !isEditor && <Loading />
: (!isEditor && (
<AppHeader
courseNumber={courseNumber}
courseOrg={courseOrg}
courseTitle={courseTitle}
courseId={courseId}
<Header
number={courseNumber}
org={courseOrg}
title={courseTitle}
contextId={courseId}
/>
)
)}

View File

@@ -88,7 +88,7 @@ const CourseAuthoringRoutes = () => {
/>
<Route
path="editor/:blockType/:blockId?"
element={<PageWrap><EditorContainer courseId={courseId} /></PageWrap>}
element={<PageWrap><EditorContainer learningContextId={courseId} /></PageWrap>}
/>
<Route
path="settings/details"
@@ -124,7 +124,7 @@ const CourseAuthoringRoutes = () => {
/>
<Route
path="certificates"
element={<PageWrap><Certificates courseId={courseId} /></PageWrap>}
element={getConfig().ENABLE_CERTIFICATE_PAGE === 'true' ? <PageWrap><Certificates courseId={courseId} /></PageWrap> : null}
/>
<Route
path="textbooks"

View File

@@ -21,9 +21,10 @@ jest.mock('react-router-dom', () => ({
}),
}));
// Mock the TinyMceWidget from frontend-lib-content-components
jest.mock('@edx/frontend-lib-content-components', () => ({
TinyMceWidget: () => <div>Widget</div>,
// Mock the TinyMceWidget
jest.mock('./editors/sharedComponents/TinyMceWidget', () => ({
__esModule: true, // Required to mock a default export
default: () => <div>Widget</div>,
Footer: () => <div>Footer</div>,
prepareEditorRef: jest.fn(() => ({
refReady: true,

View File

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

View File

@@ -6,7 +6,7 @@ import {
} 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';
import Placeholder from '../editors/Placeholder';
import AlertProctoringError from '../generic/AlertProctoringError';
import { useModel } from '../generic/model-store';

View File

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

@@ -79,3 +79,7 @@
color: $black;
}
}
.react-datepicker-popper {
z-index: 3;
}

View File

@@ -9,3 +9,7 @@
.mw-300px {
max-width: 300px;
}
.right-0 {
right: 0;
}

View File

@@ -1,7 +1,7 @@
import { Helmet } from 'react-helmet';
import PropTypes from 'prop-types';
import Placeholder from '@edx/frontend-lib-content-components';
import Placeholder from '../editors/Placeholder';
import { RequestStatus } from '../data/constants';
import Loading from '../generic/Loading';
import useCertificates from './hooks/useCertificates';

View File

@@ -59,10 +59,10 @@ describe('HeaderButtons Component', () => {
expect(previewLink).toHaveAttribute('href', expect.stringContaining(certificatesDataMock.courseModes[0]));
const dropdownButton = getByRole('button', { name: certificatesDataMock.courseModes[0] });
await userEvent.click(dropdownButton);
userEvent.click(dropdownButton);
const verifiedMode = await getByRole('button', { name: certificatesDataMock.courseModes[1] });
await userEvent.click(verifiedMode);
userEvent.click(verifiedMode);
await waitFor(() => {
expect(previewLink).toHaveAttribute('href', expect.stringContaining(certificatesDataMock.courseModes[1]));
@@ -78,7 +78,7 @@ describe('HeaderButtons Component', () => {
const { getByRole, queryByRole } = renderComponent();
const activationButton = getByRole('button', { name: messages.headingActionsActivate.defaultMessage });
await userEvent.click(activationButton);
userEvent.click(activationButton);
axiosMock.onPost(
getUpdateCertificateApiUrl(courseId, certificatesDataMock.certificates[0].id),
@@ -110,7 +110,7 @@ describe('HeaderButtons Component', () => {
const { getByRole, queryByRole } = renderComponent();
const deactivateButton = getByRole('button', { name: messages.headingActionsDeactivate.defaultMessage });
await userEvent.click(deactivateButton);
userEvent.click(deactivateButton);
axiosMock.onPost(
getUpdateCertificateApiUrl(courseId, certificatesDataMock.certificates[0].id),

View File

@@ -69,3 +69,8 @@ export const CLIPBOARD_STATUS = {
};
export const STRUCTURAL_XBLOCK_TYPES = ['vertical', 'sequential', 'chapter', 'course'];
export const REGEX_RULES = {
specialCharsRule: /^[a-zA-Z0-9_\-.'*~\s]+$/,
noSpaceRule: /^\S*$/,
};

View File

@@ -5,24 +5,24 @@ import type {} from 'react-select/base';
// and add our custom property 'myCustomProp' to it.
export interface TagTreeEntry {
explicit: boolean;
children: Record<string, TagTreeEntry>;
canChangeObjecttag: boolean;
canDeleteObjecttag: boolean;
explicit: boolean;
children: Record<string, TagTreeEntry>;
canChangeObjecttag: boolean;
canDeleteObjecttag: boolean;
}
export interface TaxonomySelectProps {
taxonomyId: number;
searchTerm: string;
appliedContentTagsTree: Record<string, TagTreeEntry>;
stagedContentTagsTree: Record<string, TagTreeEntry>;
checkedTags: string[];
selectCancelRef: Ref,
selectAddRef: Ref,
selectInlineAddRef: Ref,
handleCommitStagedTags: () => void;
handleCancelStagedTags: () => void;
handleSelectableBoxChange: React.ChangeEventHandler;
taxonomyId: number;
searchTerm: string;
appliedContentTagsTree: Record<string, TagTreeEntry>;
stagedContentTagsTree: Record<string, TagTreeEntry>;
checkedTags: string[];
selectCancelRef: Ref,
selectAddRef: Ref,
selectInlineAddRef: Ref,
handleCommitStagedTags: () => void;
handleCancelStagedTags: () => void;
handleSelectableBoxChange: React.ChangeEventHandler;
}
// Unfortunately the only way to specify the custom props we pass into React Select
@@ -32,11 +32,8 @@ export interface TaxonomySelectProps {
// we should change to using a 'react context' to share this data within <ContentTagsCollapsible>,
// rather than using the custom <Select> Props (selectProps).
declare module 'react-select/base' {
export interface Props<
Option,
IsMulti extends boolean,
Group extends GroupBase<Option>
> extends TaxonomySelectProps {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
export interface Props<Option, IsMulti extends boolean, Group extends GroupBase<Option>> extends TaxonomySelectProps {
}
}

View File

@@ -12,9 +12,10 @@ import {
Icon,
} from '@openedx/paragon';
import { Tag, KeyboardArrowDown, KeyboardArrowUp } from '@openedx/paragon/icons';
import { SelectableBox } from '@edx/frontend-lib-content-components';
import { useIntl } from '@edx/frontend-platform/i18n';
import { debounce } from 'lodash';
import SelectableBox from '../editors/sharedComponents/SelectableBox';
import messages from './messages';
import ContentTagsDropDownSelector from './ContentTagsDropDownSelector';
@@ -74,7 +75,7 @@ const CustomMenu = (props) => {
<div className="d-flex flex-row justify-content-end">
<div className="d-inline">
<Button
tabIndex="0"
tabIndex={0}
ref={selectCancelRef}
variant="tertiary"
className="tags-drawer-cancel-button"
@@ -83,7 +84,7 @@ const CustomMenu = (props) => {
{ intl.formatMessage(messages.collapsibleCancelStagedTagsButtonText) }
</Button>
<Button
tabIndex="0"
tabIndex={0}
ref={selectAddRef}
variant="tertiary"
className="text-info-500 add-tags-button"
@@ -139,7 +140,7 @@ const CustomIndicatorsContainer = (props) => {
onClick={handleCommitStagedTags}
onMouseDown={(e) => { e.stopPropagation(); e.preventDefault(); }}
ref={selectInlineAddRef}
tabIndex="0"
tabIndex={0}
onKeyDown={disableActionKeys} // To prevent navigating staged tags when button focused
>
{ intl.formatMessage(messages.collapsibleInlineAddStagedTagsButtonText) }
@@ -240,7 +241,7 @@ const ContentTagsCollapsible = ({
const selectCancelRef = React.useRef(/** @type {HTMLSelectElement | null} */(null));
const selectAddRef = React.useRef(/** @type {HTMLSelectElement | null} */(null));
const selectInlineAddRef = React.useRef(/** @type {HTMLSelectElement | null} */(null));
const selectInlineEditModeRef = React.useRef(/** @type {HTMLSelectElement | null} */(null));
const selectInlineEditModeRef = React.useRef(/** @type {HTMLButtonElement | null} */(null));
const selectRef = React.useRef(/** @type {HTMLSelectElement | null} */(null));
const [selectMenuIsOpen, setSelectMenuIsOpen] = React.useState(false);
@@ -392,16 +393,18 @@ const ContentTagsCollapsible = ({
&& (
<div className="mb-3" key={taxonomyId}>
<p className="text-gray-500">{intl.formatMessage(messages.collapsibleNoTagsAddedText)}
<Button
tabIndex="0"
size="inline"
ref={selectInlineEditModeRef}
variant="link"
className="text-info-500 add-tags-button"
onClick={toEditMode}
>
{ intl.formatMessage(messages.collapsibleAddStagedTagsButtonText) }
</Button>
{canTagObject && (
<Button
tabIndex={0}
size="inline"
ref={selectInlineEditModeRef}
variant="link"
className="text-info-500 add-tags-button"
onClick={toEditMode}
>
{ intl.formatMessage(messages.collapsibleAddStagedTagsButtonText) }
</Button>
)}
</p>
</div>
)}
@@ -417,7 +420,7 @@ const ContentTagsCollapsible = ({
)}
<div className="d-flex taxonomy-tags-selector-menu">
{isEditMode && canTagObject && (
{isEditMode && (
<Select
onBlur={handleOnBlur}
styles={{

View File

@@ -280,6 +280,30 @@ describe('<ContentTagsCollapsible />', () => {
expect(data.toEditMode).toHaveBeenCalledTimes(1);
});
it('should not render "add tags" button when expanded and not allowed to tag objects', async () => {
await getComponent({
...data,
isEditMode: false,
taxonomyAndTagsData: {
id: 123,
name: 'Taxonomy 1',
canTagObject: false,
contentTags: [],
},
});
const expandToggle = screen.getByRole('button', {
name: /taxonomy 1/i,
});
fireEvent.click(expandToggle);
expect(screen.queryByText(/no tags added yet/i)).toBeInTheDocument();
const addTags = screen.queryByRole('button', {
name: /add tags/i,
});
expect(addTags).not.toBeInTheDocument();
});
it('should call `openCollapsible` when click in the collapsible', async () => {
await getComponent({
...data,
@@ -396,7 +420,7 @@ describe('<ContentTagsCollapsible />', () => {
expect(data.removeGlobalStagedContentTag).toHaveBeenCalledWith(taxonomyId, 'Tag 3');
});
it('should call `addRemovedContentTag` when a feched tag is deleted', async () => {
it('should call `addRemovedContentTag` when a fetched tag is deleted', async () => {
await getComponent();
const tag = screen.getByText(/tag 2/i);

View File

@@ -116,7 +116,7 @@ const useContentTagsCollapsibleHelper = (
// State to keep track of the staged tags (and along with ancestors) that should be removed
const [stagedTagsToRemove, setStagedTagsToRemove] = React.useState(/** @type string[] */([]));
// State to keep track of the global tags (stagged and feched) that should be removed
// State to keep track of the global tags (staged and fetched) that should be removed
const [globalTagsToRemove, setGlobalTagsToRemove] = React.useState(/** @type string[] */([]));
// Handles the removal of staged content tags based on what was removed
@@ -140,7 +140,7 @@ const useContentTagsCollapsibleHelper = (
// A new tag has been removed
removeGlobalStagedContentTag(id, tag);
} else if (contentTags.some(t => t.value === tag)) {
// A feched tag has been removed
// A fetched tag has been removed
addRemovedContentTag(id, tag);
}
});
@@ -157,7 +157,7 @@ const useContentTagsCollapsibleHelper = (
explicitStaged.forEach((tag) => {
if (globalStagedRemovedContentTags[id]
&& globalStagedRemovedContentTags[id].includes(tag.value)) {
// A feched tag that has been removed has been added again
// A fetched tag that has been removed has been added again
deleteRemovedContentTag(id, tag.value);
} else {
// New tag added
@@ -298,7 +298,7 @@ const useContentTagsCollapsibleHelper = (
traversal[tag].lineage = tagLineage;
}
// eslint-disable-next-line no-unused-expressions
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
isExplicit ? add(value.join(',')) : remove(value.join(','));
traversal = traversal[tag].children;
});

View File

@@ -1,218 +0,0 @@
// @ts-check
import React, { useContext, useEffect } from 'react';
import PropTypes from 'prop-types';
import {
Container,
Spinner,
Stack,
Button,
Toast,
} from '@openedx/paragon';
import { useIntl } from '@edx/frontend-platform/i18n';
import { useParams } from 'react-router-dom';
import messages from './messages';
import ContentTagsCollapsible from './ContentTagsCollapsible';
import Loading from '../generic/Loading';
import useContentTagsDrawerContext from './ContentTagsDrawerHelper';
import { ContentTagsDrawerContext, ContentTagsDrawerSheetContext } from './common/context';
/**
* Drawer with the functionality to show and manage tags in a certain content.
* It is used both in interfaces of this MFE and in edx-platform interfaces such as iframe.
* - If you want to use it as an iframe, the component obtains the `contentId` from the url parameters.
* Functions to close the drawer are handled internally.
* TODO: We can delete this method when is no longer used on edx-platform.
* - If you want to use it as react component, you need to pass the content id and the close functions
* through the component parameters.
*/
const ContentTagsDrawer = ({ id, onClose }) => {
const intl = useIntl();
// TODO: We can delete 'params' when the iframe is no longer used on edx-platform
const params = useParams();
const contentId = id ?? params.contentId;
const context = useContentTagsDrawerContext(contentId);
const { blockingSheet } = useContext(ContentTagsDrawerSheetContext);
const {
showToastAfterSave,
toReadMode,
commitGlobalStagedTagsStatus,
isContentDataLoaded,
contentName,
isTaxonomyListLoaded,
isContentTaxonomyTagsLoaded,
tagsByTaxonomy,
stagedContentTags,
collapsibleStates,
isEditMode,
commitGlobalStagedTags,
toEditMode,
toastMessage,
closeToast,
setCollapsibleToInitalState,
otherTaxonomies,
} = context;
let onCloseDrawer = onClose;
if (onCloseDrawer === undefined) {
onCloseDrawer = () => {
// "*" allows communication with any origin
window.parent.postMessage('closeManageTagsDrawer', '*');
};
}
useEffect(() => {
const handleEsc = (event) => {
/* Close drawer when ESC-key is pressed and selectable dropdown box not open */
const selectableBoxOpen = document.querySelector('[data-selectable-box="taxonomy-tags"]');
if (event.key === 'Escape' && !selectableBoxOpen && !blockingSheet) {
onCloseDrawer();
}
};
document.addEventListener('keydown', handleEsc);
return () => {
document.removeEventListener('keydown', handleEsc);
};
}, [blockingSheet]);
useEffect(() => {
/* istanbul ignore next */
if (commitGlobalStagedTagsStatus === 'success') {
showToastAfterSave();
toReadMode();
}
}, [commitGlobalStagedTagsStatus]);
// First call of the initial collapsible states
React.useEffect(() => {
setCollapsibleToInitalState();
}, [isTaxonomyListLoaded, isContentTaxonomyTagsLoaded]);
return (
<ContentTagsDrawerContext.Provider value={context}>
<div id="content-tags-drawer" className="mt-1 tags-drawer d-flex flex-column justify-content-between min-vh-100 pt-3">
<Container size="xl">
{ isContentDataLoaded
? <h2 className="h3 pl-2.5">{ contentName }</h2>
: (
<div className="d-flex justify-content-center align-items-center flex-column">
<Spinner
animation="border"
size="xl"
screenReaderText={intl.formatMessage(messages.loadingMessage)}
/>
</div>
)}
<hr />
<Container>
<p className="h4 text-gray-500 font-weight-bold">
{intl.formatMessage(messages.headerSubtitle)}
</p>
{ isTaxonomyListLoaded && isContentTaxonomyTagsLoaded
? tagsByTaxonomy.map((data) => (
<div key={`taxonomy-tags-collapsible-${data.id}`}>
<ContentTagsCollapsible
contentId={contentId}
taxonomyAndTagsData={data}
stagedContentTags={stagedContentTags[data.id] || []}
collapsibleState={collapsibleStates[data.id] || false}
/>
<hr />
</div>
))
: <Loading />}
{otherTaxonomies.length !== 0 && (
<div>
<p className="h4 text-gray-500 font-weight-bold">
{intl.formatMessage(messages.otherTagsHeader)}
</p>
<p className="other-description text-gray-500">
{intl.formatMessage(messages.otherTagsDescription)}
</p>
{ isTaxonomyListLoaded && isContentTaxonomyTagsLoaded && (
otherTaxonomies.map((data) => (
<div key={`taxonomy-tags-collapsible-${data.id}`}>
<ContentTagsCollapsible
contentId={contentId}
taxonomyAndTagsData={data}
stagedContentTags={stagedContentTags[data.id] || []}
collapsibleState={collapsibleStates[data.id] || false}
/>
<hr />
</div>
))
)}
</div>
)}
</Container>
</Container>
{ isTaxonomyListLoaded && isContentTaxonomyTagsLoaded && (
<Container
className="bg-white position-sticky p-3.5 box-shadow-up-2 tags-drawer-footer"
>
<div className="d-flex justify-content-end">
{ commitGlobalStagedTagsStatus !== 'loading' ? (
<Stack direction="horizontal" gap={2}>
<Button
className="font-weight-bold tags-drawer-cancel-button"
variant="tertiary"
onClick={isEditMode
? toReadMode
: onCloseDrawer}
>
{ intl.formatMessage(isEditMode
? messages.tagsDrawerCancelButtonText
: messages.tagsDrawerCloseButtonText)}
</Button>
<Button
variant="dark"
className="rounded-0"
onClick={isEditMode
? commitGlobalStagedTags
: toEditMode}
>
{ intl.formatMessage(isEditMode
? messages.tagsDrawerSaveButtonText
: messages.tagsDrawerEditTagsButtonText)}
</Button>
</Stack>
)
: (
<Spinner
animation="border"
size="xl"
screenReaderText={intl.formatMessage(messages.loadingMessage)}
/>
)}
</div>
</Container>
)}
{/* istanbul ignore next */
toastMessage && (
<Toast
show
onClose={closeToast}
>
{toastMessage}
</Toast>
)
}
</div>
</ContentTagsDrawerContext.Provider>
);
};
ContentTagsDrawer.propTypes = {
id: PropTypes.string,
onClose: PropTypes.func,
};
ContentTagsDrawer.defaultProps = {
id: undefined,
onClose: undefined,
};
export default ContentTagsDrawer;

View File

@@ -2,7 +2,7 @@
min-width: max(500px, 33vw);
}
@media only screen and (max-width: 500px) {
@media only screen and (width <= 500px) {
.pgn__sheet-component:has(#content-tags-drawer) {
min-width: 100vw;
}
@@ -22,6 +22,11 @@
.other-description {
font-size: .9rem;
}
.enable-taxonomies-button:not([disabled]):hover {
background-color: transparent;
color: $info-900 !important;
}
}
// Apply styles to sheet only if it has a child with a .tags-drawer class

View File

@@ -1,509 +1,119 @@
import React from 'react';
import { IntlProvider } from '@edx/frontend-platform/i18n';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import {
act,
fireEvent,
initializeMocks,
render,
waitFor,
screen,
within,
} from '@testing-library/react';
} from '../testUtils';
import ContentTagsDrawer from './ContentTagsDrawer';
import {
useContentTaxonomyTagsData,
useContentData,
useTaxonomyTagsData,
useContentTaxonomyTagsUpdater,
} from './data/apiHooks';
import { getTaxonomyListData } from '../taxonomy/data/api';
import messages from './messages';
import { ContentTagsDrawerSheetContext } from './common/context';
import {
mockContentData,
mockContentTaxonomyTagsData,
mockTaxonomyListData,
mockTaxonomyTagsData,
} from './data/api.mocks';
import { getContentTaxonomyTagsApiUrl } from './data/api';
const contentId = 'block-v1:SampleTaxonomyOrg1+STC1+2023_1+type@vertical+block@7f47fe2dbcaf47c5a071671c741fe1ab';
const path = '/content/:contentId/*';
const mockOnClose = jest.fn();
const mockMutate = jest.fn();
const mockSetBlockingSheet = jest.fn();
const mockNavigate = jest.fn();
mockContentTaxonomyTagsData.applyMock();
mockTaxonomyListData.applyMock();
mockTaxonomyTagsData.applyMock();
mockContentData.applyMock();
const {
stagedTagsId,
otherTagsId,
languageWithTagsId,
languageWithoutTagsId,
largeTagsId,
emptyTagsId,
} = mockContentTaxonomyTagsData;
jest.mock('react-router-dom', () => ({
...jest.requireActual('react-router-dom'),
useParams: () => ({
contentId,
}),
useNavigate: () => mockNavigate,
}));
// FIXME: replace these mocks with API mocks
jest.mock('./data/apiHooks', () => ({
useContentTaxonomyTagsData: jest.fn(() => {}),
useContentData: jest.fn(() => ({
isSuccess: false,
data: {},
})),
useContentTaxonomyTagsUpdater: jest.fn(() => ({
isError: false,
mutate: mockMutate,
})),
useTaxonomyTagsData: jest.fn(() => ({
hasMorePages: false,
tagPages: {
isLoading: true,
isError: false,
canAddTag: false,
data: [],
},
})),
}));
jest.mock('../taxonomy/data/api', () => ({
// By default, the mock taxonomy list will never load (promise never resolves):
getTaxonomyListData: jest.fn(),
}));
const queryClient = new QueryClient();
const RootWrapper = (params) => (
<ContentTagsDrawerSheetContext.Provider value={params}>
<IntlProvider locale="en" messages={{}}>
<QueryClientProvider client={queryClient}>
<ContentTagsDrawer {...params} />
</QueryClientProvider>
</IntlProvider>
</ContentTagsDrawerSheetContext.Provider>
const renderDrawer = (contentId, drawerParams = {}) => (
render(
<ContentTagsDrawerSheetContext.Provider value={drawerParams}>
<ContentTagsDrawer {...drawerParams} />
</ContentTagsDrawerSheetContext.Provider>,
{ path, params: { contentId } },
)
);
describe('<ContentTagsDrawer />', () => {
beforeEach(async () => {
jest.clearAllMocks();
await queryClient.resetQueries();
// By default, we mock the API call with a promise that never resolves.
// You can override this in specific test.
getTaxonomyListData.mockReturnValue(new Promise(() => {}));
useContentTaxonomyTagsUpdater.mockReturnValue({
isError: false,
mutate: mockMutate,
});
initializeMocks();
});
const setupMockDataForStagedTagsTesting = () => {
useContentTaxonomyTagsData.mockReturnValue({
isSuccess: true,
data: {
taxonomies: [
{
name: 'Taxonomy 1',
taxonomyId: 123,
canTagObject: true,
tags: [
{
value: 'Tag 1',
lineage: ['Tag 1'],
canDeleteObjecttag: true,
},
{
value: 'Tag 2',
lineage: ['Tag 2'],
canDeleteObjecttag: true,
},
],
},
],
},
});
getTaxonomyListData.mockResolvedValue({
results: [
{
id: 123,
name: 'Taxonomy 1',
description: 'This is a description 1',
canTagObject: true,
},
],
});
useTaxonomyTagsData.mockReturnValue({
hasMorePages: false,
canAddTag: false,
tagPages: {
isLoading: false,
isError: false,
data: [{
value: 'Tag 1',
externalId: null,
childCount: 0,
depth: 0,
parentValue: null,
id: 12345,
subTagsUrl: null,
canChangeTag: false,
canDeleteTag: false,
}, {
value: 'Tag 2',
externalId: null,
childCount: 0,
depth: 0,
parentValue: null,
id: 12346,
subTagsUrl: null,
canChangeTag: false,
canDeleteTag: false,
}, {
value: 'Tag 3',
externalId: null,
childCount: 0,
depth: 0,
parentValue: null,
id: 12347,
subTagsUrl: null,
canChangeTag: false,
canDeleteTag: false,
}],
},
});
};
const setupMockDataWithOtherTagsTestings = () => {
useContentTaxonomyTagsData.mockReturnValue({
isSuccess: true,
data: {
taxonomies: [
{
name: 'Taxonomy 1',
taxonomyId: 123,
canTagObject: true,
tags: [
{
value: 'Tag 1',
lineage: ['Tag 1'],
canDeleteObjecttag: true,
},
{
value: 'Tag 2',
lineage: ['Tag 2'],
canDeleteObjecttag: true,
},
],
},
{
name: 'Taxonomy 2',
taxonomyId: 1234,
canTagObject: false,
tags: [
{
value: 'Tag 3',
lineage: ['Tag 3'],
canDeleteObjecttag: true,
},
{
value: 'Tag 4',
lineage: ['Tag 4'],
canDeleteObjecttag: true,
},
],
},
],
},
});
getTaxonomyListData.mockResolvedValue({
results: [
{
id: 123,
name: 'Taxonomy 1',
description: 'This is a description 1',
canTagObject: true,
},
],
});
useTaxonomyTagsData.mockReturnValue({
hasMorePages: false,
canAddTag: false,
tagPages: {
isLoading: false,
isError: false,
data: [{
value: 'Tag 1',
externalId: null,
childCount: 0,
depth: 0,
parentValue: null,
id: 12345,
subTagsUrl: null,
canChangeTag: false,
canDeleteTag: false,
}, {
value: 'Tag 2',
externalId: null,
childCount: 0,
depth: 0,
parentValue: null,
id: 12346,
subTagsUrl: null,
canChangeTag: false,
canDeleteTag: false,
}, {
value: 'Tag 3',
externalId: null,
childCount: 0,
depth: 0,
parentValue: null,
id: 12347,
subTagsUrl: null,
canChangeTag: false,
canDeleteTag: false,
}],
},
});
};
const setupLargeMockDataForStagedTagsTesting = () => {
useContentTaxonomyTagsData.mockReturnValue({
isSuccess: true,
data: {
taxonomies: [
{
name: 'Taxonomy 1',
taxonomyId: 123,
canTagObject: true,
tags: [
{
value: 'Tag 1',
lineage: ['Tag 1'],
canDeleteObjecttag: true,
},
{
value: 'Tag 2',
lineage: ['Tag 2'],
canDeleteObjecttag: true,
},
],
},
{
name: 'Taxonomy 2',
taxonomyId: 124,
canTagObject: true,
tags: [
{
value: 'Tag 1',
lineage: ['Tag 1'],
canDeleteObjecttag: true,
},
],
},
{
name: 'Taxonomy 3',
taxonomyId: 125,
canTagObject: true,
tags: [
{
value: 'Tag 1.1.1',
lineage: ['Tag 1', 'Tag 1.1', 'Tag 1.1.1'],
canDeleteObjecttag: true,
},
],
},
{
name: '(B) Taxonomy 4',
taxonomyId: 126,
canTagObject: true,
tags: [],
},
{
name: '(A) Taxonomy 5',
taxonomyId: 127,
canTagObject: true,
tags: [],
},
],
},
});
getTaxonomyListData.mockResolvedValue({
results: [
{
id: 123,
name: 'Taxonomy 1',
description: 'This is a description 1',
canTagObject: true,
},
{
id: 124,
name: 'Taxonomy 2',
description: 'This is a description 2',
canTagObject: true,
},
{
id: 125,
name: 'Taxonomy 3',
description: 'This is a description 3',
canTagObject: true,
},
{
id: 127,
name: '(A) Taxonomy 5',
description: 'This is a description 5',
canTagObject: true,
},
{
id: 126,
name: '(B) Taxonomy 4',
description: 'This is a description 4',
canTagObject: true,
},
],
});
useTaxonomyTagsData.mockReturnValue({
hasMorePages: false,
canAddTag: false,
tagPages: {
isLoading: false,
isError: false,
data: [{
value: 'Tag 1',
externalId: null,
childCount: 0,
depth: 0,
parentValue: null,
id: 12345,
subTagsUrl: null,
canChangeTag: false,
canDeleteTag: false,
}, {
value: 'Tag 2',
externalId: null,
childCount: 0,
depth: 0,
parentValue: null,
id: 12346,
subTagsUrl: null,
canChangeTag: false,
canDeleteTag: false,
}, {
value: 'Tag 3',
externalId: null,
childCount: 0,
depth: 0,
parentValue: null,
id: 12347,
subTagsUrl: null,
canChangeTag: false,
canDeleteTag: false,
}],
},
});
};
it('should render page and page title correctly', () => {
setupMockDataForStagedTagsTesting();
const { getByText } = render(<RootWrapper />);
expect(getByText('Manage tags')).toBeInTheDocument();
renderDrawer(stagedTagsId);
expect(screen.getByText('Manage tags')).toBeInTheDocument();
});
it('shows spinner before the content data query is complete', async () => {
await act(async () => {
const { getAllByRole } = render(<RootWrapper />);
const spinner = getAllByRole('status')[0];
renderDrawer(stagedTagsId);
const spinner = screen.getAllByRole('status')[0];
expect(spinner.textContent).toEqual('Loading'); // Uses <Spinner />
});
});
it('shows spinner before the taxonomy tags query is complete', async () => {
await act(async () => {
const { getAllByRole } = render(<RootWrapper />);
const spinner = getAllByRole('status')[1];
renderDrawer(stagedTagsId);
const spinner = screen.getAllByRole('status')[1];
expect(spinner.textContent).toEqual('Loading...'); // Uses <Loading />
});
});
it('shows the content display name after the query is complete', async () => {
useContentData.mockReturnValue({
isSuccess: true,
data: {
displayName: 'Unit 1',
},
});
await act(async () => {
const { getByText } = render(<RootWrapper />);
expect(getByText('Unit 1')).toBeInTheDocument();
});
it('shows the content display name after the query is complete in drawer variant', async () => {
renderDrawer('test');
expect(await screen.findByText('Loading...')).toBeInTheDocument();
expect(await screen.findByText('Unit 1')).toBeInTheDocument();
expect(await screen.findByText('Manage tags')).toBeInTheDocument();
});
it('shows the content display name after the query is complete in component variant', async () => {
renderDrawer('test', { variant: 'component' });
expect(await screen.findByText('Loading...')).toBeInTheDocument();
expect(screen.queryByText('Unit 1')).not.toBeInTheDocument();
expect(screen.queryByText('Manage tags')).not.toBeInTheDocument();
});
it('shows content using params', async () => {
useContentData.mockReturnValue({
isSuccess: true,
data: {
displayName: 'Unit 1',
},
});
render(<RootWrapper id={contentId} />);
expect(screen.getByText('Unit 1')).toBeInTheDocument();
renderDrawer(undefined, { id: 'test' });
expect(await screen.findByText('Loading...')).toBeInTheDocument();
expect(await screen.findByText('Unit 1')).toBeInTheDocument();
expect(await screen.findByText('Manage tags')).toBeInTheDocument();
});
it('shows the taxonomies data including tag numbers after the query is complete', async () => {
useContentTaxonomyTagsData.mockReturnValue({
isSuccess: true,
data: {
taxonomies: [
{
name: 'Taxonomy 1',
taxonomyId: 123,
canTagObject: true,
tags: [
{
value: 'Tag 1',
lineage: ['Tag 1'],
canDeleteObjecttag: true,
},
{
value: 'Tag 2',
lineage: ['Tag 2'],
canDeleteObjecttag: true,
},
],
},
{
name: 'Taxonomy 2',
taxonomyId: 124,
canTagObject: true,
tags: [
{
value: 'Tag 3',
lineage: ['Tag 3'],
canDeleteObjecttag: true,
},
],
},
],
},
});
getTaxonomyListData.mockResolvedValue({
results: [{
id: 123,
name: 'Taxonomy 1',
description: 'This is a description 1',
canTagObject: false,
}, {
id: 124,
name: 'Taxonomy 2',
description: 'This is a description 2',
canTagObject: false,
}],
});
await act(async () => {
const { container, getByText } = render(<RootWrapper />);
await waitFor(() => { expect(getByText('Taxonomy 1')).toBeInTheDocument(); });
expect(getByText('Taxonomy 1')).toBeInTheDocument();
expect(getByText('Taxonomy 2')).toBeInTheDocument();
const { container } = renderDrawer(largeTagsId);
await waitFor(() => { expect(screen.getByText('Taxonomy 1')).toBeInTheDocument(); });
expect(screen.getByText('Taxonomy 1')).toBeInTheDocument();
expect(screen.getByText('Taxonomy 2')).toBeInTheDocument();
const tagCountBadges = container.getElementsByClassName('taxonomy-tags-count-chip');
expect(tagCountBadges[0].textContent).toBe('2');
expect(tagCountBadges[1].textContent).toBe('1');
expect(tagCountBadges[0].textContent).toBe('3');
expect(tagCountBadges[1].textContent).toBe('2');
});
});
it('should be read only on first render', async () => {
setupMockDataForStagedTagsTesting();
render(<RootWrapper />);
it('should be read only on first render on drawer variant', async () => {
renderDrawer(stagedTagsId);
expect(await screen.findByText('Taxonomy 1')).toBeInTheDocument();
expect(screen.getByRole('button', { name: /close/i }));
expect(screen.getByRole('button', { name: /edit tags/i }));
// Not show delete tag buttons
expect(screen.queryByRole('button', { name: /delete/i })).not.toBeInTheDocument();
@@ -518,9 +128,26 @@ describe('<ContentTagsDrawer />', () => {
expect(screen.queryByRole('button', { name: /save/i })).not.toBeInTheDocument();
});
it('should change to edit mode when click on `Edit tags`', async () => {
setupMockDataForStagedTagsTesting();
render(<RootWrapper />);
it('should be read only on first render on component variant', async () => {
renderDrawer(stagedTagsId, { variant: 'component' });
expect(await screen.findByText('Taxonomy 1')).toBeInTheDocument();
expect(screen.getByRole('button', { name: /manage tags/i }));
// Not show delete tag buttons
expect(screen.queryByRole('button', { name: /delete/i })).not.toBeInTheDocument();
// Not show add a tag select
expect(screen.queryByText(/add a tag/i)).not.toBeInTheDocument();
// Not show cancel button
expect(screen.queryByRole('button', { name: /cancel/i })).not.toBeInTheDocument();
// Not show save button
expect(screen.queryByRole('button', { name: /save/i })).not.toBeInTheDocument();
});
it('should change to edit mode when click on `Edit tags` on drawer variant', async () => {
renderDrawer(stagedTagsId);
expect(await screen.findByText('Taxonomy 1')).toBeInTheDocument();
const editTagsButton = screen.getByRole('button', {
name: /edit tags/i,
@@ -542,9 +169,31 @@ describe('<ContentTagsDrawer />', () => {
expect(screen.getByRole('button', { name: /save/i })).toBeInTheDocument();
});
it('should change to read mode when click on `Cancel`', async () => {
setupMockDataForStagedTagsTesting();
render(<RootWrapper />);
it('should change to edit mode when click on `Manage tags` on component variant', async () => {
renderDrawer(stagedTagsId, { variant: 'component' });
expect(await screen.findByText('Taxonomy 1')).toBeInTheDocument();
const manageTagsButton = screen.getByRole('button', {
name: /manage tags/i,
});
fireEvent.click(manageTagsButton);
// Show delete tag buttons
expect(screen.getAllByRole('button', {
name: /delete/i,
}).length).toBe(2);
// Show add a tag select
expect(screen.getByText(/add a tag/i)).toBeInTheDocument();
// Show cancel button
expect(screen.getByRole('button', { name: /cancel/i })).toBeInTheDocument();
// Show save button
expect(screen.getByRole('button', { name: /save/i })).toBeInTheDocument();
});
it('should change to read mode when click on `Cancel` on drawer variant', async () => {
renderDrawer(stagedTagsId);
expect(await screen.findByText('Taxonomy 1')).toBeInTheDocument();
const editTagsButton = screen.getByRole('button', {
name: /edit tags/i,
@@ -569,21 +218,57 @@ describe('<ContentTagsDrawer />', () => {
expect(screen.queryByRole('button', { name: /save/i })).not.toBeInTheDocument();
});
it('shows spinner when loading commit tags', async () => {
setupMockDataForStagedTagsTesting();
useContentTaxonomyTagsUpdater.mockReturnValue({
status: 'loading',
isError: false,
mutate: mockMutate,
});
render(<RootWrapper />);
it('should change to read mode when click on `Cancel` on component variant', async () => {
renderDrawer(stagedTagsId, { variant: 'component' });
expect(await screen.findByText('Taxonomy 1')).toBeInTheDocument();
expect(screen.getByRole('status')).toBeInTheDocument();
const manageTagsButton = screen.getByRole('button', {
name: /manage tags/i,
});
fireEvent.click(manageTagsButton);
const cancelButton = screen.getByRole('button', {
name: /cancel/i,
});
fireEvent.click(cancelButton);
// Not show delete tag buttons
expect(screen.queryByRole('button', { name: /delete/i })).not.toBeInTheDocument();
// Not show add a tag select
expect(screen.queryByText(/add a tag/i)).not.toBeInTheDocument();
// Not show cancel button
expect(screen.queryByRole('button', { name: /cancel/i })).not.toBeInTheDocument();
// Not show save button
expect(screen.queryByRole('button', { name: /save/i })).not.toBeInTheDocument();
});
test.each([
{
variant: 'drawer',
editButton: /edit tags/i,
},
{
variant: 'component',
editButton: /manage tags/i,
},
])(
'should hide "$editButton" button on $variant variant if not allowed to tag object',
async ({ variant, editButton }) => {
renderDrawer(stagedTagsId, { variant, readOnly: true });
expect(await screen.findByText('Taxonomy 1')).toBeInTheDocument();
expect(screen.queryByRole('button', { name: editButton })).not.toBeInTheDocument();
expect(screen.queryByRole('button', { name: /delete/i })).not.toBeInTheDocument();
expect(screen.queryByText(/add a tag/i)).not.toBeInTheDocument();
expect(screen.queryByRole('button', { name: /cancel/i })).not.toBeInTheDocument();
expect(screen.queryByRole('button', { name: /save/i })).not.toBeInTheDocument();
},
);
it('should test adding a content tag to the staged tags for a taxonomy', async () => {
setupMockDataForStagedTagsTesting();
render(<RootWrapper />);
renderDrawer(stagedTagsId);
expect(await screen.findByText('Taxonomy 1')).toBeInTheDocument();
// To edit mode
@@ -598,7 +283,7 @@ describe('<ContentTagsDrawer />', () => {
fireEvent.mouseDown(addTagsButton);
// Tag 3 should only appear in dropdown selector, (i.e. the dropdown is open, since Tag 3 is not applied)
expect(screen.getAllByText('Tag 3').length).toBe(1);
expect((await screen.findAllByText('Tag 3')).length).toBe(1);
// Click to check Tag 3
const tag3 = screen.getByText('Tag 3');
@@ -609,8 +294,7 @@ describe('<ContentTagsDrawer />', () => {
});
it('should test removing a staged content from a taxonomy', async () => {
setupMockDataForStagedTagsTesting();
render(<RootWrapper />);
renderDrawer(stagedTagsId);
expect(await screen.findByText('Taxonomy 1')).toBeInTheDocument();
// To edit mode
@@ -625,7 +309,7 @@ describe('<ContentTagsDrawer />', () => {
fireEvent.mouseDown(addTagsButton);
// Tag 3 should only appear in dropdown selector, (i.e. the dropdown is open, since Tag 3 is not applied)
expect(screen.getAllByText('Tag 3').length).toBe(1);
expect((await screen.findAllByText('Tag 3')).length).toBe(1);
// Click to check Tag 3
const tag3 = screen.getByText('Tag 3');
@@ -640,11 +324,9 @@ describe('<ContentTagsDrawer />', () => {
});
it('should test clearing staged tags for a taxonomy', async () => {
setupMockDataForStagedTagsTesting();
const {
container,
} = render(<RootWrapper />);
} = renderDrawer(stagedTagsId);
expect(await screen.findByText('Taxonomy 1')).toBeInTheDocument();
// To edit mode
@@ -659,7 +341,7 @@ describe('<ContentTagsDrawer />', () => {
fireEvent.mouseDown(addTagsButton);
// Tag 3 should only appear in dropdown selector, (i.e. the dropdown is open, since Tag 3 is not applied)
expect(screen.getAllByText('Tag 3').length).toBe(1);
expect((await screen.findAllByText('Tag 3')).length).toBe(1);
// Click to check Tag 3
const tag3 = screen.getByText('Tag 3');
@@ -678,8 +360,7 @@ describe('<ContentTagsDrawer />', () => {
});
it('should test adding global staged tags and cancel', async () => {
setupMockDataForStagedTagsTesting();
render(<RootWrapper />);
renderDrawer(stagedTagsId);
expect(await screen.findByText('Taxonomy 1')).toBeInTheDocument();
// To edit mode
@@ -694,7 +375,7 @@ describe('<ContentTagsDrawer />', () => {
fireEvent.mouseDown(addTagsButton);
// Click to check Tag 3
const tag3 = screen.getByText(/tag 3/i);
const tag3 = await screen.findByText(/tag 3/i);
fireEvent.click(tag3);
// Click "Add tags" to save to global staged tags
@@ -710,9 +391,8 @@ describe('<ContentTagsDrawer />', () => {
expect(screen.queryByText(/tag 3/i)).not.toBeInTheDocument();
});
it('should test delete feched tags and cancel', async () => {
setupMockDataForStagedTagsTesting();
render(<RootWrapper />);
it('should test delete fetched tags and cancel', async () => {
renderDrawer(stagedTagsId);
expect(await screen.findByText('Taxonomy 1')).toBeInTheDocument();
// To edit mode
@@ -722,7 +402,7 @@ describe('<ContentTagsDrawer />', () => {
fireEvent.click(editTagsButton);
// Delete the tag
const tag = screen.getByText(/tag 2/i);
const tag = await screen.findByText(/tag 2/i);
const deleteButton = within(tag).getByRole('button', {
name: /delete/i,
});
@@ -738,8 +418,7 @@ describe('<ContentTagsDrawer />', () => {
});
it('should test delete global staged tags and cancel', async () => {
setupMockDataForStagedTagsTesting();
render(<RootWrapper />);
renderDrawer(stagedTagsId);
expect(await screen.findByText('Taxonomy 1')).toBeInTheDocument();
// To edit mode
@@ -754,7 +433,7 @@ describe('<ContentTagsDrawer />', () => {
fireEvent.mouseDown(addTagsButton);
// Click to check Tag 3
const tag3 = screen.getByText(/tag 3/i);
const tag3 = await screen.findByText(/tag 3/i);
fireEvent.click(tag3);
// Click "Add tags" to save to global staged tags
@@ -779,9 +458,8 @@ describe('<ContentTagsDrawer />', () => {
expect(screen.queryByText(/tag 3/i)).not.toBeInTheDocument();
});
it('should test add removed feched tags and cancel', async () => {
setupMockDataForStagedTagsTesting();
render(<RootWrapper />);
it('should test add removed fetched tags and cancel', async () => {
renderDrawer(stagedTagsId);
expect(await screen.findByText('Taxonomy 1')).toBeInTheDocument();
// To edit mode
@@ -791,7 +469,7 @@ describe('<ContentTagsDrawer />', () => {
fireEvent.click(editTagsButton);
// Delete the tag
const tag = screen.getByText(/tag 2/i);
const tag = await screen.findByText(/tag 2/i);
const deleteButton = within(tag).getByRole('button', {
name: /delete/i,
});
@@ -805,7 +483,7 @@ describe('<ContentTagsDrawer />', () => {
fireEvent.mouseDown(addTagsButton);
// Click to check Tag 2
const tag2 = screen.getByText(/tag 2/i);
const tag2 = await screen.findByText(/tag 2/i);
fireEvent.click(tag2);
// Click "Add tags" to save to global staged tags
@@ -822,8 +500,7 @@ describe('<ContentTagsDrawer />', () => {
});
it('should call onClose when cancel is clicked', async () => {
setupMockDataForStagedTagsTesting();
render(<RootWrapper onClose={mockOnClose} />);
renderDrawer(stagedTagsId, { onClose: mockOnClose });
const cancelButton = await screen.findByRole('button', {
name: /close/i,
@@ -837,7 +514,7 @@ describe('<ContentTagsDrawer />', () => {
it('should call closeManageTagsDrawer when Escape key is pressed and no selectable box is active', () => {
const postMessageSpy = jest.spyOn(window.parent, 'postMessage');
const { container } = render(<RootWrapper />);
const { container } = renderDrawer(stagedTagsId);
fireEvent.keyDown(container, {
key: 'Escape',
@@ -849,7 +526,7 @@ describe('<ContentTagsDrawer />', () => {
});
it('should call `onClose` when Escape key is pressed and no selectable box is active', () => {
const { container } = render(<RootWrapper onClose={mockOnClose} />);
const { container } = renderDrawer(stagedTagsId, { onClose: mockOnClose });
fireEvent.keyDown(container, {
key: 'Escape',
@@ -861,7 +538,7 @@ describe('<ContentTagsDrawer />', () => {
it('should not call closeManageTagsDrawer when Escape key is pressed and a selectable box is active', () => {
const postMessageSpy = jest.spyOn(window.parent, 'postMessage');
const { container } = render(<RootWrapper />);
const { container } = renderDrawer(stagedTagsId);
// Simulate that the selectable box is open by adding an element with the data attribute
const selectableBox = document.createElement('div');
@@ -881,7 +558,7 @@ describe('<ContentTagsDrawer />', () => {
});
it('should not call `onClose` when Escape key is pressed and a selectable box is active', () => {
const { container } = render(<RootWrapper onClose={mockOnClose} />);
const { container } = renderDrawer(stagedTagsId, { onClose: mockOnClose });
// Simulate that the selectable box is open by adding an element with the data attribute
const selectableBox = document.createElement('div');
@@ -900,8 +577,7 @@ describe('<ContentTagsDrawer />', () => {
it('should not call closeManageTagsDrawer when Escape key is pressed and container is blocked', () => {
const postMessageSpy = jest.spyOn(window.parent, 'postMessage');
const { container } = render(<RootWrapper blockingSheet />);
const { container } = renderDrawer(stagedTagsId, { blockingSheet: true });
fireEvent.keyDown(container, {
key: 'Escape',
});
@@ -912,7 +588,10 @@ describe('<ContentTagsDrawer />', () => {
});
it('should not call `onClose` when Escape key is pressed and container is blocked', () => {
const { container } = render(<RootWrapper blockingSheet onClose={mockOnClose} />);
const { container } = renderDrawer(stagedTagsId, {
blockingSheet: true,
onClose: mockOnClose,
});
fireEvent.keyDown(container, {
key: 'Escape',
});
@@ -921,8 +600,10 @@ describe('<ContentTagsDrawer />', () => {
});
it('should call `setBlockingSheet` on add a tag', async () => {
setupMockDataForStagedTagsTesting();
render(<RootWrapper blockingSheet setBlockingSheet={mockSetBlockingSheet} />);
renderDrawer(stagedTagsId, {
blockingSheet: true,
setBlockingSheet: mockSetBlockingSheet,
});
expect(await screen.findByText('Taxonomy 1')).toBeInTheDocument();
expect(mockSetBlockingSheet).toHaveBeenCalledWith(false);
@@ -939,7 +620,7 @@ describe('<ContentTagsDrawer />', () => {
fireEvent.mouseDown(addTagsButton);
// Click to check Tag 3
const tag3 = screen.getByText(/tag 3/i);
const tag3 = await screen.findByText(/tag 3/i);
fireEvent.click(tag3);
// Click "Add tags" to save to global staged tags
@@ -950,8 +631,10 @@ describe('<ContentTagsDrawer />', () => {
});
it('should call `setBlockingSheet` on delete a tag', async () => {
setupMockDataForStagedTagsTesting();
render(<RootWrapper blockingSheet setBlockingSheet={mockSetBlockingSheet} />);
renderDrawer(stagedTagsId, {
blockingSheet: true,
setBlockingSheet: mockSetBlockingSheet,
});
expect(await screen.findByText('Taxonomy 1')).toBeInTheDocument();
expect(mockSetBlockingSheet).toHaveBeenCalledWith(false);
@@ -973,8 +656,10 @@ describe('<ContentTagsDrawer />', () => {
});
it('should call `updateTags` mutation on save', async () => {
setupMockDataForStagedTagsTesting();
render(<RootWrapper />);
const { axiosMock } = initializeMocks();
const url = getContentTaxonomyTagsApiUrl(stagedTagsId);
axiosMock.onPut(url).reply(200);
renderDrawer(stagedTagsId);
expect(await screen.findByText('Taxonomy 1')).toBeInTheDocument();
const editTagsButton = screen.getByRole('button', {
name: /edit tags/i,
@@ -986,12 +671,11 @@ describe('<ContentTagsDrawer />', () => {
});
fireEvent.click(saveButton);
expect(mockMutate).toHaveBeenCalled();
await waitFor(() => expect(axiosMock.history.put[0].url).toEqual(url));
});
it('should taxonomies must be ordered', async () => {
setupLargeMockDataForStagedTagsTesting();
render(<RootWrapper />);
renderDrawer(largeTagsId);
expect(await screen.findByText('Taxonomy 1')).toBeInTheDocument();
// First, taxonomies with content sorted by count implicit
@@ -1011,18 +695,14 @@ describe('<ContentTagsDrawer />', () => {
});
it('should not show "Other tags" section', async () => {
setupMockDataForStagedTagsTesting();
render(<RootWrapper />);
renderDrawer(stagedTagsId);
expect(await screen.findByText('Taxonomy 1')).toBeInTheDocument();
expect(screen.queryByText('Other tags')).not.toBeInTheDocument();
});
it('should show "Other tags" section', async () => {
setupMockDataWithOtherTagsTestings();
render(<RootWrapper />);
renderDrawer(otherTagsId);
expect(await screen.findByText('Taxonomy 1')).toBeInTheDocument();
expect(screen.getByText('Other tags')).toBeInTheDocument();
@@ -1032,8 +712,7 @@ describe('<ContentTagsDrawer />', () => {
});
it('should test delete "Other tags" and cancel', async () => {
setupMockDataWithOtherTagsTestings();
render(<RootWrapper />);
renderDrawer(otherTagsId);
expect(await screen.findByText('Taxonomy 2')).toBeInTheDocument();
// To edit mode
@@ -1057,4 +736,25 @@ describe('<ContentTagsDrawer />', () => {
expect(screen.getByText(/tag 3/i)).toBeInTheDocument();
});
it('should show Language Taxonomy', async () => {
renderDrawer(languageWithTagsId);
expect(await screen.findByText('Languages')).toBeInTheDocument();
});
it('should hide Language Taxonomy', async () => {
renderDrawer(languageWithoutTagsId);
expect(await screen.findByText('Taxonomy 1')).toBeInTheDocument();
expect(screen.queryByText('Languages')).not.toBeInTheDocument();
});
it('should show empty drawer message', async () => {
renderDrawer(emptyTagsId);
expect(await screen.findByText(/to use tags, please or contact your administrator\./i)).toBeInTheDocument();
const enableButton = screen.getByRole('button', {
name: /enable a taxonomy/i,
});
fireEvent.click(enableButton);
expect(mockNavigate).toHaveBeenCalledWith('/taxonomies');
});
});

View File

@@ -0,0 +1,399 @@
import React, { useContext, useEffect } from 'react';
import {
Container,
Spinner,
Stack,
Button,
Toast,
} from '@openedx/paragon';
import { useIntl, FormattedMessage } from '@edx/frontend-platform/i18n';
import { useParams, useNavigate } from 'react-router-dom';
import classNames from 'classnames';
import messages from './messages';
import ContentTagsCollapsible from './ContentTagsCollapsible';
import Loading from '../generic/Loading';
import useContentTagsDrawerContext from './ContentTagsDrawerHelper';
import { ContentTagsDrawerContext, ContentTagsDrawerSheetContext } from './common/context';
interface TaxonomyListProps {
contentId: string;
}
const TaxonomyList = ({ contentId }: TaxonomyListProps) => {
const navigate = useNavigate();
const intl = useIntl();
const {
isTaxonomyListLoaded,
isContentTaxonomyTagsLoaded,
tagsByTaxonomy,
stagedContentTags,
collapsibleStates,
} = React.useContext(ContentTagsDrawerContext);
if (isTaxonomyListLoaded && isContentTaxonomyTagsLoaded) {
if (tagsByTaxonomy.length !== 0) {
return (
<div>
{ tagsByTaxonomy.map((data) => (
<div key={data.id}>
<ContentTagsCollapsible
contentId={contentId}
taxonomyAndTagsData={data}
stagedContentTags={stagedContentTags[data.id] || []}
collapsibleState={collapsibleStates[data.id] || false}
/>
<hr />
</div>
))}
</div>
);
}
return (
<FormattedMessage
{...messages.emptyDrawerContent}
values={{
link: (
<Button
tabIndex={0}
size="inline"
variant="link"
className="text-info-500 p-0 enable-taxonomies-button"
onClick={() => navigate('/taxonomies')}
>
{ intl.formatMessage(messages.emptyDrawerContentLink) }
</Button>
),
}}
/>
);
}
return <Loading />;
};
const ContentTagsDrawerTitle = () => {
const intl = useIntl();
const {
isContentDataLoaded,
contentName,
} = useContext(ContentTagsDrawerContext);
return (
<>
{ isContentDataLoaded
? <h2 className="h3 pl-2.5">{ contentName }</h2>
: (
<div className="d-flex justify-content-center align-items-center flex-column">
<Spinner
animation="border"
size="xl"
screenReaderText={intl.formatMessage(messages.loadingMessage)}
/>
</div>
)}
<hr />
</>
);
};
interface ContentTagsDrawerVariantFooterProps {
onClose: () => void,
readOnly: boolean,
}
const ContentTagsDrawerVariantFooter = ({ onClose, readOnly }: ContentTagsDrawerVariantFooterProps) => {
const intl = useIntl();
const {
commitGlobalStagedTagsStatus,
commitGlobalStagedTags,
isEditMode,
toReadMode,
toEditMode,
} = useContext(ContentTagsDrawerContext);
return (
<Container
className="bg-white position-sticky p-3.5 box-shadow-up-2 tags-drawer-footer"
>
<div className="d-flex justify-content-end">
{ commitGlobalStagedTagsStatus !== 'loading' ? (
<Stack direction="horizontal" gap={2}>
<Button
className="font-weight-bold tags-drawer-cancel-button"
variant="tertiary"
onClick={isEditMode
? toReadMode
: onClose}
>
{ intl.formatMessage(isEditMode
? messages.tagsDrawerCancelButtonText
: messages.tagsDrawerCloseButtonText)}
</Button>
{!readOnly && (
<Button
className="rounded-0"
onClick={isEditMode
? commitGlobalStagedTags
: toEditMode}
>
{ intl.formatMessage(isEditMode
? messages.tagsDrawerSaveButtonText
: messages.tagsDrawerEditTagsButtonText)}
</Button>
)}
</Stack>
)
: (
<Spinner
animation="border"
size="xl"
screenReaderText={intl.formatMessage(messages.loadingMessage)}
/>
)}
</div>
</Container>
);
};
interface ContentTagsComponentVariantFooterProps {
readOnly?: boolean;
}
const ContentTagsComponentVariantFooter = ({ readOnly = false }: ContentTagsComponentVariantFooterProps) => {
const intl = useIntl();
const {
commitGlobalStagedTagsStatus,
commitGlobalStagedTags,
isEditMode,
toReadMode,
toEditMode,
} = useContext(ContentTagsDrawerContext);
return (
<div>
{isEditMode ? (
<div>
{ commitGlobalStagedTagsStatus !== 'loading' ? (
<Stack direction="horizontal" gap={2}>
<Button
className="font-weight-bold tags-drawer-cancel-button"
variant="tertiary"
onClick={toReadMode}
>
{intl.formatMessage(messages.tagsDrawerCancelButtonText)}
</Button>
<Button
className="rounded-0"
onClick={commitGlobalStagedTags}
block
>
{intl.formatMessage(messages.tagsDrawerSaveButtonText)}
</Button>
</Stack>
) : (
<div className="d-flex justify-content-center">
<Spinner
animation="border"
size="xl"
screenReaderText={intl.formatMessage(messages.loadingMessage)}
/>
</div>
)}
</div>
) : !readOnly && (
<Button
variant="outline-primary"
onClick={toEditMode}
block
>
{intl.formatMessage(messages.manageTagsButton)}
</Button>
)}
</div>
);
};
interface ContentTagsDrawerProps {
id?: string;
onClose?: () => void;
variant?: 'drawer' | 'component';
readOnly?: boolean;
}
/**
* Drawer with the functionality to show and manage tags in a certain content.
* It is used both in interfaces of this MFE and in edx-platform interfaces such as iframe.
* - If you want to use it as an iframe, the component obtains the `contentId` from the url parameters.
* Functions to close the drawer are handled internally.
* TODO: We can delete this method when is no longer used on edx-platform.
* - If you want to use it as react component, you need to pass the content id and the close functions
* through the component parameters.
*/
const ContentTagsDrawer = ({
id,
onClose,
variant = 'drawer',
readOnly = false,
}: ContentTagsDrawerProps) => {
const intl = useIntl();
// TODO: We can delete 'params' when the iframe is no longer used on edx-platform
const params = useParams();
const contentId = id ?? params.contentId;
if (contentId === undefined) {
throw new Error('Error: contentId cannot be null.');
}
const context = useContentTagsDrawerContext(contentId, !readOnly);
const { blockingSheet } = useContext(ContentTagsDrawerSheetContext);
const {
showToastAfterSave,
toReadMode,
commitGlobalStagedTagsStatus,
isTaxonomyListLoaded,
isContentTaxonomyTagsLoaded,
stagedContentTags,
collapsibleStates,
toastMessage,
closeToast,
setCollapsibleToInitalState,
otherTaxonomies,
} = context;
let onCloseDrawer: () => void;
if (variant === 'drawer') {
if (onClose === undefined) {
onCloseDrawer = () => {
// "*" allows communication with any origin
window.parent.postMessage('closeManageTagsDrawer', '*');
};
} else {
onCloseDrawer = onClose;
}
}
useEffect(() => {
if (variant === 'drawer') {
const handleEsc = (event) => {
/* Close drawer when ESC-key is pressed and selectable dropdown box not open */
const selectableBoxOpen = document.querySelector('[data-selectable-box="taxonomy-tags"]');
if (event.key === 'Escape' && !selectableBoxOpen && !blockingSheet) {
onCloseDrawer();
}
};
document.addEventListener('keydown', handleEsc);
return () => {
document.removeEventListener('keydown', handleEsc);
};
}
return () => {};
}, [blockingSheet]);
useEffect(() => {
/* istanbul ignore next */
if (commitGlobalStagedTagsStatus === 'success') {
showToastAfterSave();
toReadMode();
}
}, [commitGlobalStagedTagsStatus]);
// First call of the initial collapsible states
React.useEffect(() => {
setCollapsibleToInitalState();
}, [isTaxonomyListLoaded, isContentTaxonomyTagsLoaded]);
const renderFooter = () => {
if (isTaxonomyListLoaded && isContentTaxonomyTagsLoaded) {
switch (variant) {
case 'drawer':
return <ContentTagsDrawerVariantFooter onClose={onCloseDrawer} readOnly={readOnly} />;
case 'component':
return <ContentTagsComponentVariantFooter readOnly={readOnly} />;
default:
return null;
}
}
return null;
};
return (
<ContentTagsDrawerContext.Provider value={context}>
<div
id="content-tags-drawer"
className={classNames(
'mt-1 tags-drawer d-flex flex-column justify-content-between pt-3',
{
'min-vh-100': variant === 'drawer',
},
)}
>
<Container
size="xl"
className={classNames(
{
'p-0': variant === 'component',
},
)}
>
{variant === 'drawer' && (
<ContentTagsDrawerTitle />
)}
<Container
className={classNames(
{
'p-0': variant === 'component',
},
)}
>
{variant === 'drawer' && (
<p className="h4 text-gray-500 font-weight-bold">
{intl.formatMessage(messages.headerSubtitle)}
</p>
)}
<TaxonomyList contentId={contentId} />
{otherTaxonomies.length !== 0 && (
<div>
<p className="h4 text-gray-500 font-weight-bold">
{intl.formatMessage(messages.otherTagsHeader)}
</p>
<p className="other-description text-gray-500">
{intl.formatMessage(messages.otherTagsDescription)}
</p>
{ isTaxonomyListLoaded && isContentTaxonomyTagsLoaded && (
otherTaxonomies.map((data) => (
<div key={data.id}>
<ContentTagsCollapsible
contentId={contentId}
taxonomyAndTagsData={data}
stagedContentTags={stagedContentTags[data.id] || []}
collapsibleState={collapsibleStates[data.id] || false}
/>
<hr />
</div>
))
)}
</div>
)}
</Container>
</Container>
{renderFooter()}
{/* istanbul ignore next */
toastMessage && (
<Toast
show
onClose={closeToast}
>
{toastMessage}
</Toast>
)
}
</div>
</ContentTagsDrawerContext.Provider>
);
};
export default ContentTagsDrawer;

View File

@@ -4,7 +4,7 @@ import { useIntl } from '@edx/frontend-platform/i18n';
import { cloneDeep } from 'lodash';
import { useContentData, useContentTaxonomyTagsData, useContentTaxonomyTagsUpdater } from './data/apiHooks';
import { useTaxonomyList } from '../taxonomy/data/apiHooks';
import { extractOrgFromContentId } from './utils';
import { extractOrgFromContentId, languageExportId } from './utils';
import messages from './messages';
import { ContentTagsDrawerSheetContext } from './common/context';
@@ -15,6 +15,7 @@ import { ContentTagsDrawerSheetContext } from './common/context';
/**
* Handles the context and all the underlying logic for the ContentTagsDrawer component
* @param {string} contentId
* @param {boolean} canTagObject
* @returns {{
* stagedContentTags: Record<number, StagedTagData[]>,
* addStagedContentTag: (taxonomyId: number, addedTag: StagedTagData) => void,
@@ -46,7 +47,7 @@ import { ContentTagsDrawerSheetContext } from './common/context';
* otherTaxonomies: TagsInTaxonomy[],
* }}
*/
const useContentTagsDrawerContext = (contentId) => {
const useContentTagsDrawerContext = (contentId, canTagObject) => {
const intl = useIntl();
const org = extractOrgFromContentId(contentId);
@@ -58,9 +59,9 @@ const useContentTagsDrawerContext = (contentId) => {
const [stagedContentTags, setStagedContentTags] = React.useState({});
// When a staged tags on a taxonomy is commitet then is saved on this map.
const [globalStagedContentTags, setGlobalStagedContentTags] = React.useState({});
// This stores feched tags deleted by the user.
// This stores fetched tags deleted by the user.
const [globalStagedRemovedContentTags, setGlobalStagedRemovedContentTags] = React.useState({});
// Merges feched tags, global staged tags and global removed staged tags
// Merges fetched tags, global staged tags and global removed staged tags
const [tagsByTaxonomy, setTagsByTaxonomy] = React.useState(/** @type TagsInTaxonomy[] */ ([]));
// Other taxonomies that the user doesn't have permissions
const [otherTaxonomies, setOtherTaxonomies] = React.useState(/** @type TagsInTaxonomy[] */ ([]));
@@ -79,8 +80,8 @@ const useContentTagsDrawerContext = (contentId) => {
} = useContentTaxonomyTagsData(contentId);
const { data: taxonomyListData, isSuccess: isTaxonomyListLoaded } = useTaxonomyList(org);
// Tags feched from database
const { fechedTaxonomies, fechedOtherTaxonomies } = React.useMemo(() => {
// Tags fetched from database
const { fetchedTaxonomies, fetchedOtherTaxonomies } = React.useMemo(() => {
const sortTaxonomies = (taxonomiesList) => {
const taxonomiesWithData = taxonomiesList.filter(
(t) => t.contentTags.length !== 0,
@@ -115,6 +116,7 @@ const useContentTagsDrawerContext = (contentId) => {
// Initialize list of content tags in taxonomies to populate
const taxonomiesList = taxonomyListData.results.map((taxonomy) => ({
...taxonomy,
canTagObject: taxonomy.canTagObject && canTagObject,
contentTags: /** @type {ContentTagData[]} */([]),
}));
@@ -142,14 +144,20 @@ const useContentTagsDrawerContext = (contentId) => {
}
});
// Delete Language taxonomy if is empty
const filteredTaxonomies = taxonomiesList.filter(
(taxonomy) => taxonomy.exportId !== languageExportId
|| taxonomy.contentTags.length !== 0,
);
return {
fechedTaxonomies: sortTaxonomies(taxonomiesList),
fechedOtherTaxonomies: otherTaxonomiesList,
fetchedTaxonomies: sortTaxonomies(filteredTaxonomies),
fetchedOtherTaxonomies: otherTaxonomiesList,
};
}
return {
fechedTaxonomies: [],
fechedOtherTaxonomies: [],
fetchedTaxonomies: [],
fetchedOtherTaxonomies: [],
};
}, [taxonomyListData, contentTaxonomyTagsData]);
@@ -224,28 +232,28 @@ const useContentTagsDrawerContext = (contentId) => {
const openAllCollapsible = React.useCallback(() => {
const updatedState = {};
fechedTaxonomies.forEach((taxonomy) => {
fetchedTaxonomies.forEach((taxonomy) => {
updatedState[taxonomy.id] = true;
});
fechedOtherTaxonomies.forEach((taxonomy) => {
fetchedOtherTaxonomies.forEach((taxonomy) => {
updatedState[taxonomy.id] = true;
});
setColapsibleStates(updatedState);
}, [fechedTaxonomies, setColapsibleStates]);
}, [fetchedTaxonomies, setColapsibleStates]);
// Set initial state of collapsible based on content tags
const setCollapsibleToInitalState = React.useCallback(() => {
const updatedState = {};
fechedTaxonomies.forEach((taxonomy) => {
fetchedTaxonomies.forEach((taxonomy) => {
// Taxonomy with content tags must be open
updatedState[taxonomy.id] = taxonomy.contentTags.length !== 0;
});
fechedOtherTaxonomies.forEach((taxonomy) => {
fetchedOtherTaxonomies.forEach((taxonomy) => {
// Taxonomy with content tags must be open
updatedState[taxonomy.id] = taxonomy.contentTags.length !== 0;
});
setColapsibleStates(updatedState);
}, [fechedTaxonomies, setColapsibleStates]);
}, [fetchedTaxonomies, setColapsibleStates]);
// Changes the drawer mode to edit
const toEditMode = React.useCallback(() => {
@@ -325,7 +333,7 @@ const useContentTagsDrawerContext = (contentId) => {
const closeToast = React.useCallback(() => setToastMessage(undefined), [setToastMessage]);
let contentName = '';
if (isContentDataLoaded) {
if (isContentDataLoaded && contentData) {
if ('displayName' in contentData) {
contentName = contentData.displayName;
} else {
@@ -333,14 +341,14 @@ const useContentTagsDrawerContext = (contentId) => {
}
}
// Updates `tagsByTaxonomy` merged feched tags, global staged tags
// Updates `tagsByTaxonomy` merged fetched tags, global staged tags
// and global removed staged tags.
React.useEffect(() => {
const mergedTags = cloneDeep(fechedTaxonomies).reduce((acc, obj) => (
const mergedTags = cloneDeep(fetchedTaxonomies).reduce((acc, obj) => (
{ ...acc, [obj.id]: obj }
), {});
const mergedOtherTaxonomies = cloneDeep(fechedOtherTaxonomies).reduce((acc, obj) => (
const mergedOtherTaxonomies = cloneDeep(fetchedOtherTaxonomies).reduce((acc, obj) => (
{ ...acc, [obj.id]: obj }
), {});
@@ -349,10 +357,10 @@ const useContentTagsDrawerContext = (contentId) => {
// TODO test this
// Filter out applied tags that should become implicit because a child tag was committed
const stagedLineages = globalStagedContentTags[taxonomyId].map((t) => t.lineage.slice(0, -1)).flat();
const fechedTags = mergedTags[taxonomyId].contentTags.filter((t) => !stagedLineages.includes(t.value));
const fetchedTags = mergedTags[taxonomyId].contentTags.filter((t) => !stagedLineages.includes(t.value));
mergedTags[taxonomyId].contentTags = [
...fechedTags,
...fetchedTags,
...globalStagedContentTags[taxonomyId],
];
}
@@ -371,8 +379,8 @@ const useContentTagsDrawerContext = (contentId) => {
});
// It is constructed this way to maintain the order
// of the list `fechedTaxonomies`
const mergedTagsArray = fechedTaxonomies.map(obj => mergedTags[obj.id]);
// of the list `fetchedTaxonomies`
const mergedTagsArray = fetchedTaxonomies.map(obj => mergedTags[obj.id]);
setTagsByTaxonomy(mergedTagsArray);
setOtherTaxonomies(Object.values(mergedOtherTaxonomies));
@@ -402,8 +410,8 @@ const useContentTagsDrawerContext = (contentId) => {
}
}
}, [
fechedTaxonomies,
fechedOtherTaxonomies,
fetchedTaxonomies,
fetchedOtherTaxonomies,
globalStagedContentTags,
globalStagedRemovedContentTags,
]);

View File

@@ -12,6 +12,10 @@ const ContentTagsDrawerSheet = ({ id, onClose, showSheet }) => {
blockingSheet, setBlockingSheet,
}), [blockingSheet, setBlockingSheet]);
// ContentTagsDrawerSheet is only used when editing Courses/Course Units,
// so we assume it's ok to edit the object tags too.
const readOnly = false;
return (
<ContentTagsDrawerSheetContext.Provider value={context}>
<Sheet
@@ -23,6 +27,7 @@ const ContentTagsDrawerSheet = ({ id, onClose, showSheet }) => {
<ContentTagsDrawer
id={id}
onClose={onClose}
readOnly={readOnly}
/>
</Sheet>
</ContentTagsDrawerSheetContext.Provider>

View File

@@ -5,13 +5,13 @@ import {
Spinner,
Button,
} from '@openedx/paragon';
import { SelectableBox } from '@edx/frontend-lib-content-components';
import { useIntl, FormattedMessage } from '@edx/frontend-platform/i18n';
import { ArrowDropDown, ArrowDropUp, Add } from '@openedx/paragon/icons';
import PropTypes from 'prop-types';
import messages from './messages';
import SelectableBox from '../editors/sharedComponents/SelectableBox';
import { useTaxonomyTagsData } from './data/apiHooks';
import messages from './messages';
const HighlightedText = ({ text, highlight }) => {
if (!highlight) {
@@ -309,7 +309,7 @@ const ContentTagsDropDownSelector = ({
? (
<div>
<Button
tabIndex="0"
tabIndex={0}
variant="tertiary"
iconBefore={Add}
onClick={loadMoreTags}
@@ -323,7 +323,9 @@ const ContentTagsDropDownSelector = ({
{ tagPages.data.length === 0 && !tagPages.isLoading && (
<div className="d-flex justify-content-center muted-text">
<FormattedMessage {...messages.noTagsFoundMessage} values={{ searchTerm }} />
{ searchTerm
? <FormattedMessage {...messages.noTagsFoundMessage} values={{ searchTerm }} />
: <FormattedMessage {...messages.noTagsInTaxonomyMessage} />}
</div>
)}

View File

@@ -282,4 +282,28 @@ describe('<ContentTagsDropDownSelector />', () => {
expect(getByText(message)).toBeInTheDocument();
});
});
it('should render "noTagsInTaxonomy" message if taxonomy is empty', async () => {
useTaxonomyTagsData.mockReturnValueOnce({
hasMorePages: false,
tagPages: {
isLoading: false,
isError: false,
isSuccess: true,
data: [],
},
});
const searchTerm = '';
await act(async () => {
const { getByText } = await getComponent({ ...data, searchTerm });
await waitFor(() => {
expect(useTaxonomyTagsData).toBeCalledWith(data.taxonomyId, null, 1, searchTerm);
});
const message = 'No tags in this taxonomy yet';
expect(getByText(message)).toBeInTheDocument();
});
});
});

View File

@@ -72,10 +72,16 @@ export async function getContentTaxonomyTagsCount(contentId) {
/**
* Fetch meta data (eg: display_name) about the content object (unit/compoenent)
* @param {string} contentId The id of the content object (unit/component)
* @returns {Promise<import("./types.mjs").ContentData>}
* @returns {Promise<import("./types.mjs").ContentData | null>}
*/
export async function getContentData(contentId) {
let url;
if (contentId.startsWith('lib-collection:')) {
// This type of usage_key is not used to obtain collections
// is only used in tagging.
return null;
}
if (contentId.startsWith('lb:')) {
url = getLibraryContentDataApiUrl(contentId);
} else if (contentId.startsWith('course-v1:')) {

View File

@@ -0,0 +1,378 @@
import * as api from './api';
import * as taxonomyApi from '../../taxonomy/data/api';
import { languageExportId } from '../utils';
/**
* Mock for `getContentTaxonomyTagsData()`
*/
export async function mockContentTaxonomyTagsData(contentId: string): Promise<any> {
const thisMock = mockContentTaxonomyTagsData;
switch (contentId) {
case thisMock.stagedTagsId: return thisMock.stagedTags;
case thisMock.otherTagsId: return thisMock.otherTags;
case thisMock.languageWithTagsId: return thisMock.languageWithTags;
case thisMock.languageWithoutTagsId: return thisMock.languageWithoutTags;
case thisMock.largeTagsId: return thisMock.largeTags;
case thisMock.emptyTagsId: return thisMock.emptyTags;
default: throw new Error(`No mock has been set up for contentId "${contentId}"`);
}
}
mockContentTaxonomyTagsData.stagedTagsId = 'block-v1:StagedTagsOrg+STC1+2023_1+type@vertical+block@stagedTagsId';
mockContentTaxonomyTagsData.stagedTags = {
taxonomies: [
{
name: 'Taxonomy 1',
taxonomyId: 123,
canTagObject: true,
tags: [
{
value: 'Tag 1',
lineage: ['Tag 1'],
canDeleteObjecttag: true,
},
{
value: 'Tag 2',
lineage: ['Tag 2'],
canDeleteObjecttag: true,
},
],
},
],
};
mockContentTaxonomyTagsData.otherTagsId = 'block-v1:StagedTagsOrg+STC1+2023_1+type@vertical+block@otherTagsId';
mockContentTaxonomyTagsData.otherTags = {
taxonomies: [
{
name: 'Taxonomy 1',
taxonomyId: 123,
canTagObject: true,
tags: [
{
value: 'Tag 1',
lineage: ['Tag 1'],
canDeleteObjecttag: true,
},
{
value: 'Tag 2',
lineage: ['Tag 2'],
canDeleteObjecttag: true,
},
],
},
{
name: 'Taxonomy 2',
taxonomyId: 1234,
canTagObject: false,
tags: [
{
value: 'Tag 3',
lineage: ['Tag 3'],
canDeleteObjecttag: true,
},
{
value: 'Tag 4',
lineage: ['Tag 4'],
canDeleteObjecttag: true,
},
],
},
],
};
mockContentTaxonomyTagsData.languageWithTagsId = 'block-v1:LanguageTagsOrg+STC1+2023_1+type@vertical+block@languageWithTagsId';
mockContentTaxonomyTagsData.languageWithTags = {
taxonomies: [
{
name: 'Languages',
taxonomyId: 1234,
exportId: languageExportId,
canTagObject: true,
tags: [
{
value: 'Tag 1',
lineage: ['Tag 1'],
canDeleteObjecttag: true,
},
],
},
{
name: 'Taxonomy 1',
taxonomyId: 12345,
canTagObject: true,
tags: [
{
value: 'Tag 1',
lineage: ['Tag 1'],
canDeleteObjecttag: true,
},
{
value: 'Tag 2',
lineage: ['Tag 2'],
canDeleteObjecttag: true,
},
],
},
],
};
mockContentTaxonomyTagsData.languageWithoutTagsId = 'block-v1:LanguageTagsOrg+STC1+2023_1+type@vertical+block@languageWithoutTagsId';
mockContentTaxonomyTagsData.languageWithoutTags = {
taxonomies: [
{
name: 'Languages',
taxonomyId: 1234,
exportId: languageExportId,
canTagObject: true,
tags: [],
},
{
name: 'Taxonomy 1',
taxonomyId: 12345,
canTagObject: true,
tags: [
{
value: 'Tag 1',
lineage: ['Tag 1'],
canDeleteObjecttag: true,
},
{
value: 'Tag 2',
lineage: ['Tag 2'],
canDeleteObjecttag: true,
},
],
},
],
};
mockContentTaxonomyTagsData.largeTagsId = 'block-v1:LargeTagsOrg+STC1+2023_1+type@vertical+block@largeTagsId';
mockContentTaxonomyTagsData.largeTags = {
taxonomies: [
{
name: 'Taxonomy 1',
taxonomyId: 123,
canTagObject: true,
tags: [
{
value: 'Tag 1',
lineage: ['Tag 1'],
canDeleteObjecttag: true,
},
{
value: 'Tag 2',
lineage: ['Tag 2'],
canDeleteObjecttag: true,
},
],
},
{
name: 'Taxonomy 2',
taxonomyId: 124,
canTagObject: true,
tags: [
{
value: 'Tag 1',
lineage: ['Tag 1'],
canDeleteObjecttag: true,
},
],
},
{
name: 'Taxonomy 3',
taxonomyId: 125,
canTagObject: true,
tags: [
{
value: 'Tag 1.1.1',
lineage: ['Tag 1', 'Tag 1.1', 'Tag 1.1.1'],
canDeleteObjecttag: true,
},
],
},
{
name: '(B) Taxonomy 4',
taxonomyId: 126,
canTagObject: true,
tags: [],
},
{
name: '(A) Taxonomy 5',
taxonomyId: 127,
canTagObject: true,
tags: [],
},
],
};
mockContentTaxonomyTagsData.emptyTagsId = 'block-v1:EmptyTagsOrg+STC1+2023_1+type@vertical+block@emptyTagsId';
mockContentTaxonomyTagsData.emptyTags = {
taxonomies: [],
};
mockContentTaxonomyTagsData.applyMock = () => jest.spyOn(api, 'getContentTaxonomyTagsData').mockImplementation(mockContentTaxonomyTagsData);
/**
* Mock for `getTaxonomyListData()`
*/
export async function mockTaxonomyListData(org: string): Promise<any> {
const thisMock = mockTaxonomyListData;
switch (org) {
case thisMock.stagedTagsOrg: return thisMock.stagedTags;
case thisMock.languageTagsOrg: return thisMock.languageTags;
case thisMock.largeTagsOrg: return thisMock.largeTags;
case thisMock.emptyTagsOrg: return thisMock.emptyTags;
default: throw new Error(`No mock has been set up for org "${org}"`);
}
}
mockTaxonomyListData.stagedTagsOrg = 'StagedTagsOrg';
mockTaxonomyListData.stagedTags = {
results: [
{
id: 123,
name: 'Taxonomy 1',
description: 'This is a description 1',
canTagObject: true,
},
],
};
mockTaxonomyListData.languageTagsOrg = 'LanguageTagsOrg';
mockTaxonomyListData.languageTags = {
results: [
{
id: 1234,
name: 'Languages',
description: 'This is a description 1',
exportId: languageExportId,
canTagObject: true,
},
{
id: 12345,
name: 'Taxonomy 1',
description: 'This is a description 2',
canTagObject: true,
},
],
};
mockTaxonomyListData.largeTagsOrg = 'LargeTagsOrg';
mockTaxonomyListData.largeTags = {
results: [
{
id: 123,
name: 'Taxonomy 1',
description: 'This is a description 1',
canTagObject: true,
},
{
id: 124,
name: 'Taxonomy 2',
description: 'This is a description 2',
canTagObject: true,
},
{
id: 125,
name: 'Taxonomy 3',
description: 'This is a description 3',
canTagObject: true,
},
{
id: 127,
name: '(A) Taxonomy 5',
description: 'This is a description 5',
canTagObject: true,
},
{
id: 126,
name: '(B) Taxonomy 4',
description: 'This is a description 4',
canTagObject: true,
},
],
};
mockTaxonomyListData.emptyTagsOrg = 'EmptyTagsOrg';
mockTaxonomyListData.emptyTags = {
results: [],
};
mockTaxonomyListData.applyMock = () => jest.spyOn(taxonomyApi, 'getTaxonomyListData').mockImplementation(mockTaxonomyListData);
/**
* Mock for `getTaxonomyTagsData()`
*/
export async function mockTaxonomyTagsData(taxonomyId: number): Promise<any> {
const thisMock = mockTaxonomyTagsData;
switch (taxonomyId) {
case thisMock.stagedTagsTaxonomy: return thisMock.stagedTags;
case thisMock.languageTagsTaxonomy: return thisMock.languageTags;
default: throw new Error(`No mock has been set up for taxonomyId "${taxonomyId}"`);
}
}
mockTaxonomyTagsData.stagedTagsTaxonomy = 123;
mockTaxonomyTagsData.stagedTags = {
count: 3,
currentPage: 1,
next: null,
numPages: 1,
previous: null,
start: 1,
results: [
{
value: 'Tag 1',
externalId: null,
childCount: 0,
depth: 0,
parentValue: null,
id: 12345,
subTagsUrl: null,
canChangeTag: false,
canDeleteTag: false,
},
{
value: 'Tag 2',
externalId: null,
childCount: 0,
depth: 0,
parentValue: null,
id: 12346,
subTagsUrl: null,
canChangeTag: false,
canDeleteTag: false,
},
{
value: 'Tag 3',
externalId: null,
childCount: 0,
depth: 0,
parentValue: null,
id: 12347,
subTagsUrl: null,
canChangeTag: false,
canDeleteTag: false,
},
],
};
mockTaxonomyTagsData.languageTagsTaxonomy = 1234;
mockTaxonomyTagsData.languageTags = {
count: 1,
currentPage: 1,
next: null,
numPages: 1,
previous: null,
start: 1,
results: [{
value: 'Tag 1',
externalId: null,
childCount: 0,
depth: 0,
parentValue: null,
id: 12345,
subTagsUrl: null,
canChangeTag: false,
canDeleteTag: false,
}],
};
mockTaxonomyTagsData.applyMock = () => jest.spyOn(api, 'getTaxonomyTagsData').mockImplementation(mockTaxonomyTagsData);
/**
* Mock for `getContentData()`
*/
export async function mockContentData(): Promise<any> {
return mockContentData.data;
}
mockContentData.data = {
displayName: 'Unit 1',
};
mockContentData.applyMock = () => jest.spyOn(api, 'getContentData').mockImplementation(mockContentData);

View File

@@ -14,6 +14,8 @@ import {
updateContentTaxonomyTags,
getContentTaxonomyTagsCount,
} from './api';
import { libraryQueryPredicate, xblockQueryKeys } from '../../library-authoring/data/apiHooks';
import { getLibraryId } from '../../generic/key-utils';
/** @typedef {import("../../taxonomy/tag-list/data/types.mjs").TagListData} TagListData */
/** @typedef {import("../../taxonomy/tag-list/data/types.mjs").TagData} TagData */
@@ -146,6 +148,14 @@ export const useContentTaxonomyTagsUpdater = (contentId) => {
contentPattern = contentId.replace(/\+type@.*$/, '*');
}
queryClient.invalidateQueries({ queryKey: ['contentTagsCount', contentPattern] });
if (contentId.startsWith('lb:') || contentId.startsWith('lib-collection:')) {
// Obtain library id from contentId
const libraryId = getLibraryId(contentId);
// Invalidate component metadata to update tags count
queryClient.invalidateQueries(xblockQueryKeys.componentMetadata(contentId));
// Invalidate content search to update tags count
queryClient.invalidateQueries(['content_search'], { predicate: (query) => libraryQueryPredicate(query, libraryId) });
}
},
onSuccess: /* istanbul ignore next */ () => {
/* istanbul ignore next */

View File

@@ -25,6 +25,11 @@ const messages = defineMessages({
id: 'course-authoring.content-tags-drawer.tags-dropdown-selector.no-tags-found',
defaultMessage: 'No tags found with the search term "{searchTerm}"',
},
noTagsInTaxonomyMessage: {
id: 'course-authoring.content-tags-drawer.tags-dropdown-selector.no-tags-in-taxonomy',
defaultMessage: 'No tags in this taxonomy yet',
description: 'Message when the user uses the tags dropdown selector of an empty taxonomy',
},
taxonomyTagChecked: {
id: 'course-authoring.content-tags-drawer.tags-dropdown-selector.tag-checked',
defaultMessage: 'Checked',
@@ -64,7 +69,7 @@ const messages = defineMessages({
defaultMessage: 'Add a tag',
},
collapsibleNoTagsAddedText: {
id: 'course-authoring.content-tags-drawer.content-tags-collapsible.custom-menu.placeholder-text',
id: 'course-authoring.content-tags-drawer.content-tags-collapsible.custom-menu.no-tags-added-text',
defaultMessage: 'No tags added yet.',
},
collapsibleAddStagedTagsButtonText: {
@@ -124,6 +129,16 @@ const messages = defineMessages({
defaultMessage: 'These tags are already applied, but you can\'t add new ones as you don\'t have access to their taxonomies.',
description: 'Description of "Other tags" subsection in tags drawer',
},
emptyDrawerContent: {
id: 'course-authoring.content-tags-drawer.empty',
defaultMessage: 'To use tags, please {link} or contact your administrator.',
description: 'Message when there are no taxonomies.',
},
emptyDrawerContentLink: {
id: 'course-authoring.content-tags-drawer.empty-link',
defaultMessage: 'enable a taxonomy',
description: 'Message of the link used in empty drawer message.',
},
});
export default messages;

View File

@@ -15,7 +15,7 @@ const TagsSidebarHeader = () => {
const {
data: contentTagsCount,
isSuccess: isContentTagsCountLoaded,
} = useContentTagsCount(contentId || '');
} = useContentTagsCount(contentId);
return (
<Stack

View File

@@ -1,2 +1,3 @@
// eslint-disable-next-line import/prefer-default-export
export const extractOrgFromContentId = (contentId) => contentId.split('+')[0].split(':')[1];
export const languageExportId = 'languages-v1';

View File

@@ -79,10 +79,9 @@ const ChecklistItemComment = ({
<ul className="assignment-list">
{gradedAssignmentsOutsideDateRange.map(assignment => (
<li className="assignment-list-item" key={assignment.id}>
<Hyperlink
content={assignment.displayName}
destination={`${outlineUrl}#${assignment.id}`}
/>
<Hyperlink destination={`${outlineUrl}#${assignment.id}`}>
{assignment.displayName}
</Hyperlink>
</li>
))}
</ul>

View File

@@ -2,30 +2,30 @@ import * as healthValidators from './courseChecklistValidators';
const getValidatedValue = (data, id) => {
switch (id) {
case 'welcomeMessage':
return healthValidators.hasWelcomeMessage(data.updates);
case 'gradingPolicy':
return healthValidators.hasGradingPolicy(data.grades);
case 'certificate':
return healthValidators.hasCertificate(data.certificates);
case 'courseDates':
return healthValidators.hasDates(data.dates);
case 'assignmentDeadlines':
return healthValidators.hasAssignmentDeadlines(data.assignments, data.dates);
case 'videoDuration':
return healthValidators.hasShortVideoDuration(data.videos);
case 'mobileFriendlyVideo':
return healthValidators.hasMobileFriendlyVideos(data.videos);
case 'diverseSequences':
return healthValidators.hasDiverseSequences(data.subsections);
case 'weeklyHighlights':
return healthValidators.hasWeeklyHighlights(data.sections);
case 'unitDepth':
return healthValidators.hasShortUnitDepth(data.units);
case 'proctoringEmail':
return healthValidators.hasProctoringEscalationEmail(data.proctoring);
default:
throw new Error(`Unknown validator ${id}.`);
case 'welcomeMessage':
return healthValidators.hasWelcomeMessage(data.updates);
case 'gradingPolicy':
return healthValidators.hasGradingPolicy(data.grades);
case 'certificate':
return healthValidators.hasCertificate(data.certificates);
case 'courseDates':
return healthValidators.hasDates(data.dates);
case 'assignmentDeadlines':
return healthValidators.hasAssignmentDeadlines(data.assignments, data.dates);
case 'videoDuration':
return healthValidators.hasShortVideoDuration(data.videos);
case 'mobileFriendlyVideo':
return healthValidators.hasMobileFriendlyVideos(data.videos);
case 'diverseSequences':
return healthValidators.hasDiverseSequences(data.subsections);
case 'weeklyHighlights':
return healthValidators.hasWeeklyHighlights(data.sections);
case 'unitDepth':
return healthValidators.hasShortUnitDepth(data.units);
case 'proctoringEmail':
return healthValidators.hasProctoringEscalationEmail(data.proctoring);
default:
throw new Error(`Unknown validator ${id}.`);
}
};

View File

@@ -8,6 +8,7 @@ import {
Layout,
Row,
TransitionReplace,
Toast,
} from '@openedx/paragon';
import { Helmet } from 'react-helmet';
import {
@@ -20,6 +21,7 @@ import {
SortableContext,
verticalListSortingStrategy,
} from '@dnd-kit/sortable';
import { useLocation } from 'react-router-dom';
import { LoadingSpinner } from '../generic/Loading';
import { getProcessingNotification } from '../generic/processing-notification/data/selectors';
@@ -52,9 +54,11 @@ import {
} from '../generic/drag-helper/utils';
import { useCourseOutline } from './hooks';
import messages from './messages';
import { getTagsExportFile } from './data/api';
const CourseOutline = ({ courseId }) => {
const intl = useIntl();
const location = useLocation();
const {
courseName,
@@ -117,6 +121,24 @@ const CourseOutline = ({ courseId }) => {
errors,
} = useCourseOutline({ courseId });
// Use `setToastMessage` to show the toast.
const [toastMessage, setToastMessage] = useState(/** @type{null|string} */ (null));
useEffect(() => {
// Wait for the course data to load before exporting tags.
if (courseId && courseName && location.hash === '#export-tags') {
setToastMessage(intl.formatMessage(messages.exportTagsCreatingToastMessage));
getTagsExportFile(courseId, courseName).then(() => {
setToastMessage(intl.formatMessage(messages.exportTagsSuccessToastMessage));
}).catch(() => {
setToastMessage(intl.formatMessage(messages.exportTagsErrorToastMessage));
});
// Delete `#export-tags` from location
window.location.href = '#';
}
}, [location, courseId, courseName]);
const [sections, setSections] = useState(sectionsList);
const restoreSectionList = () => {
@@ -438,6 +460,7 @@ const CourseOutline = ({ courseId }) => {
onConfigureSubmit={handleConfigureItemSubmit}
currentItemData={currentItemData}
enableProctoredExams={enableProctoredExams}
isSelfPaced={statusBarData.isSelfPaced}
/>
<DeleteModal
category={deleteCategory}
@@ -457,6 +480,15 @@ const CourseOutline = ({ courseId }) => {
onInternetConnectionFailed={handleInternetConnectionFailed}
/>
</div>
{toastMessage && (
<Toast
show
onClose={/* istanbul ignore next */ () => setToastMessage(null)}
data-testid="taxonomy-toast"
>
{toastMessage}
</Toast>
)}
</>
);
};

View File

@@ -1,5 +1,5 @@
import {
act, render, waitFor, fireEvent, within,
act, render, waitFor, fireEvent, within, screen,
} from '@testing-library/react';
import { IntlProvider } from '@edx/frontend-platform/i18n';
import { AppProvider } from '@edx/frontend-platform/react';
@@ -10,6 +10,7 @@ import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { cloneDeep } from 'lodash';
import { closestCorners } from '@dnd-kit/core';
import { useLocation } from 'react-router-dom';
import {
getCourseBestPracticesApiUrl,
getCourseLaunchApiUrl,
@@ -19,6 +20,7 @@ import {
getCourseBlockApiUrl,
getCourseItemApiUrl,
getXBlockBaseApiUrl,
exportTags,
} from './data/api';
import { RequestStatus } from '../data/constants';
import {
@@ -74,9 +76,7 @@ global.BroadcastChannel = jest.fn(() => clipboardBroadcastChannelMock);
jest.mock('react-router-dom', () => ({
...jest.requireActual('react-router-dom'),
useLocation: () => ({
pathname: mockPathname,
}),
useLocation: jest.fn(),
}));
jest.mock('../help-urls/hooks', () => ({
@@ -135,6 +135,10 @@ describe('<CourseOutline />', () => {
},
});
useLocation.mockReturnValue({
pathname: mockPathname,
});
store = initializeStore();
axiosMock = new MockAdapter(getAuthenticatedHttpClient());
axiosMock
@@ -222,7 +226,7 @@ describe('<CourseOutline />', () => {
});
it('check video sharing option shows error on failure', async () => {
const { findByLabelText, queryByRole } = render(<RootWrapper />);
render(<RootWrapper />);
axiosMock
.onPost(getCourseBlockApiUrl(courseId), {
@@ -231,7 +235,7 @@ describe('<CourseOutline />', () => {
},
})
.reply(500);
const optionDropdown = await findByLabelText(statusBarMessages.videoSharingTitle.defaultMessage);
const optionDropdown = await screen.findByLabelText(statusBarMessages.videoSharingTitle.defaultMessage);
await act(
async () => fireEvent.change(optionDropdown, { target: { value: VIDEO_SHARING_OPTIONS.allOff } }),
);
@@ -243,8 +247,10 @@ describe('<CourseOutline />', () => {
},
}));
const alertElement = queryByRole('alert');
expect(alertElement).toHaveTextContent(
const alertElements = screen.queryAllByRole('alert');
expect(alertElements.find(
(el) => el.classList.contains('alert-content'),
)).toHaveTextContent(
pageAlertMessages.alertFailedGeneric.defaultMessage,
);
});
@@ -507,9 +513,10 @@ describe('<CourseOutline />', () => {
notificationDismissUrl: '/some/url',
});
const { findByRole } = render(<RootWrapper />);
expect(await findByRole('alert')).toBeInTheDocument();
const dismissBtn = await findByRole('button', { name: 'Dismiss' });
render(<RootWrapper />);
const alert = await screen.findByText(pageAlertMessages.configurationErrorTitle.defaultMessage);
expect(alert).toBeInTheDocument();
const dismissBtn = await screen.findByRole('button', { name: 'Dismiss' });
axiosMock
.onDelete('/some/url')
.reply(204);
@@ -1405,6 +1412,7 @@ describe('<CourseOutline />', () => {
publish: 'republish',
metadata: {
visible_to_staff_only: isVisibleToStaffOnly,
discussion_enabled: false,
group_access: newGroupAccess,
},
})
@@ -1423,6 +1431,7 @@ describe('<CourseOutline />', () => {
// after configuraiton response
unit.visibilityState = 'staff_only';
unit.discussion_enabled = false;
unit.userPartitionInfo = {
selectablePartitions: [
{
@@ -1465,6 +1474,11 @@ describe('<CourseOutline />', () => {
)).toBeInTheDocument();
let visibilityCheckbox = await within(configureModal).findByTestId('unit-visibility-checkbox');
await act(async () => fireEvent.click(visibilityCheckbox));
let discussionCheckbox = await within(configureModal).findByLabelText(
configureModalMessages.discussionEnabledCheckbox.defaultMessage,
);
expect(discussionCheckbox).toBeChecked();
await act(async () => fireEvent.click(discussionCheckbox));
let groupeType = await within(configureModal).findByTestId('group-type-select');
fireEvent.change(groupeType, { target: { value: '0' } });
@@ -1481,6 +1495,10 @@ describe('<CourseOutline />', () => {
configureModal = await findByTestId('configure-modal');
visibilityCheckbox = await within(configureModal).findByTestId('unit-visibility-checkbox');
expect(visibilityCheckbox).toBeChecked();
discussionCheckbox = await within(configureModal).findByLabelText(
configureModalMessages.discussionEnabledCheckbox.defaultMessage,
);
expect(discussionCheckbox).not.toBeChecked();
groupeType = await within(configureModal).findByTestId('group-type-select');
expect(groupeType).toHaveValue('0');
@@ -2145,10 +2163,10 @@ describe('<CourseOutline />', () => {
});
it('check whether unit copy & paste option works correctly', async () => {
const { findAllByTestId, queryByTestId, findAllByRole } = render(<RootWrapper />);
render(<RootWrapper />);
// get first section -> first subsection -> first unit element
const [section] = courseOutlineIndexMock.courseStructure.childInfo.children;
const [sectionElement] = await findAllByTestId('section-card');
const [sectionElement] = await screen.findAllByTestId('section-card');
const [subsection] = section.childInfo.children;
axiosMock
.onGet(getXBlockApiUrl(section.id))
@@ -2187,7 +2205,7 @@ describe('<CourseOutline />', () => {
await act(async () => fireEvent.mouseOver(clipboardLabel));
// find clipboard content popover link
const popoverContent = queryByTestId('popover-content');
const popoverContent = screen.queryByTestId('popover-content');
expect(popoverContent.tagName).toBe('A');
expect(popoverContent).toHaveAttribute('href', `${getConfig().STUDIO_BASE_URL}${unit.studioUrl}`);
@@ -2218,8 +2236,10 @@ describe('<CourseOutline />', () => {
errorFiles: ['error.css'],
});
let alerts = await screen.findAllByRole('alert');
// Exclude processing notification toast
alerts = alerts.filter((el) => !el.classList.contains('toast-container'));
// 3 alerts should be present
const alerts = await findAllByRole('alert');
expect(alerts.length).toEqual(3);
// check alerts for errorFiles
@@ -2237,4 +2257,38 @@ describe('<CourseOutline />', () => {
// check pasteFileNotices in store
expect(store.getState().courseOutline.pasteFileNotices).toEqual({});
});
it('should show toats on export tags', async () => {
const expectedResponse = 'this is a test';
axiosMock
.onGet(exportTags(courseId))
.reply(200, expectedResponse);
useLocation.mockReturnValue({
pathname: '/foo-bar',
hash: '#export-tags',
});
window.URL.createObjectURL = jest.fn().mockReturnValue('http://example.com/archivo');
window.URL.revokeObjectURL = jest.fn();
render(<RootWrapper />);
expect(await screen.findByText('Please wait. Creating export file for course tags...')).toBeInTheDocument();
const expectedRequest = axiosMock.history.get.filter(request => request.url === exportTags(courseId));
expect(expectedRequest.length).toBe(1);
expect(await screen.findByText('Course tags exported successfully')).toBeInTheDocument();
});
it('should show toast on export tags error', async () => {
axiosMock
.onGet(exportTags(courseId))
.reply(404);
useLocation.mockReturnValue({
pathname: '/foo-bar',
hash: '#export-tags',
});
render(<RootWrapper />);
expect(await screen.findByText('Please wait. Creating export file for course tags...')).toBeInTheDocument();
expect(await screen.findByText('An error has occurred creating the file')).toBeInTheDocument();
});
});

View File

@@ -142,7 +142,7 @@ const CardHeader = ({
{(isVertical || isSequential) && (
<CardStatus status={status} showDiscussionsEnabledBadge={showDiscussionsEnabledBadge} />
)}
{ getConfig().ENABLE_TAGGING_TAXONOMY_PAGES === 'true' && contentTagCount > 0 && (
{ getConfig().ENABLE_TAGGING_TAXONOMY_PAGES === 'true' && !!contentTagCount && (
<TagCount count={contentTagCount} onClick={openManageTagsDrawer} />
)}
<Dropdown data-testid={`${namePrefix}-card-header__menu`} onClick={onClickMenuButton}>

View File

@@ -58,7 +58,7 @@ const messages = defineMessages({
defaultMessage: 'Delete',
},
menuCopy: {
id: 'course-authoring.course-outline.card.menu.delete',
id: 'course-authoring.course-outline.card.menu.copy',
defaultMessage: 'Copy to clipboard',
},
menuProctoringLinkText: {

View File

@@ -29,6 +29,7 @@ export const getXBlockBaseApiUrl = () => `${getApiBaseUrl()}/xblock/`;
export const getCourseItemApiUrl = (itemId) => `${getXBlockBaseApiUrl()}${itemId}`;
export const getXBlockApiUrl = (blockId) => `${getXBlockBaseApiUrl()}outline/${blockId}`;
export const getClipboardUrl = () => `${getApiBaseUrl()}/api/content-staging/v1/clipboard/`;
export const exportTags = (courseId) => `${getApiBaseUrl()}/api/content_tagging/v1/object_tags/${courseId}/export/`;
/**
* @typedef {Object} courseOutline
@@ -310,7 +311,7 @@ export async function configureCourseSubsection(
* @param {object} groupAccess
* @returns {Promise<Object>}
*/
export async function configureCourseUnit(unitId, isVisibleToStaffOnly, groupAccess) {
export async function configureCourseUnit(unitId, isVisibleToStaffOnly, groupAccess, discussionEnabled) {
const { data } = await getAuthenticatedHttpClient()
.post(getCourseItemApiUrl(unitId), {
publish: 'republish',
@@ -318,6 +319,7 @@ export async function configureCourseUnit(unitId, isVisibleToStaffOnly, groupAcc
// The backend expects metadata.visible_to_staff_only to either true or null
visible_to_staff_only: isVisibleToStaffOnly ? true : null,
group_access: groupAccess,
discussion_enabled: discussionEnabled,
},
});
@@ -458,3 +460,33 @@ export async function dismissNotification(url) {
await getAuthenticatedHttpClient()
.delete(url);
}
/**
* Downloads the file of the exported tags
* @param {string} courseId The ID of the content
* @returns void
*/
export async function getTagsExportFile(courseId, courseName) {
// Gets exported tags and builds the blob to download CSV file.
// This can be done with this code:
// `window.location.href = exportTags(contentId);`
// but it is done in this way so we know when the operation ends to close the toast.
const response = await getAuthenticatedHttpClient().get(exportTags(courseId), {
responseType: 'blob',
});
/* istanbul ignore next */
if (response.status !== 200) {
throw response.statusText;
}
const blob = new Blob([response.data], { type: 'text/csv' });
const url = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `${courseName}.csv`;
a.click();
window.URL.revokeObjectURL(url);
}

View File

@@ -57,7 +57,10 @@ import {
const getErrorDetails = (error, dismissible = true) => {
const errorInfo = { dismissible };
if (error.response?.data) {
errorInfo.data = JSON.stringify(error.response.data);
const { data } = error.response;
if ((typeof data === 'string' && !data.includes('</html>')) || typeof data === 'object') {
errorInfo.data = JSON.stringify(data);
}
errorInfo.status = error.response.status;
errorInfo.type = API_ERROR_TYPES.serverError;
} else if (error.request) {
@@ -334,11 +337,11 @@ export function configureCourseSubsectionQuery(
};
}
export function configureCourseUnitQuery(itemId, sectionId, isVisibleToStaffOnly, groupAccess) {
export function configureCourseUnitQuery(itemId, sectionId, isVisibleToStaffOnly, groupAccess, discussionEnabled) {
return async (dispatch) => {
dispatch(configureCourseItemQuery(
sectionId,
async () => configureCourseUnit(itemId, isVisibleToStaffOnly, groupAccess),
async () => configureCourseUnit(itemId, isVisibleToStaffOnly, groupAccess, discussionEnabled),
));
};
}

View File

@@ -3,15 +3,11 @@ import { render, fireEvent } from '@testing-library/react';
import { IntlProvider } from '@edx/frontend-platform/i18n';
import { AppProvider } from '@edx/frontend-platform/react';
import { initializeMockApp } from '@edx/frontend-platform';
import MockAdapter from 'axios-mock-adapter';
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
import initializeStore from '../../store';
import EnableHighlightsModal from './EnableHighlightsModal';
import messages from './messages';
// eslint-disable-next-line no-unused-vars
let axiosMock;
let store;
const mockPathname = '/foo-bar';
@@ -56,7 +52,6 @@ describe('<EnableHighlightsModal />', () => {
});
store = initializeStore();
axiosMock = new MockAdapter(getAuthenticatedHttpClient());
});
it('renders EnableHighlightsModal component correctly', () => {

View File

@@ -38,6 +38,7 @@ const HighlightsModal = ({
onClose={onClose}
hasCloseButton
isFullscreenOnMobile
isOverflowVisible={false}
>
<ModalDialog.Header className="highlights-modal__header">
<ModalDialog.Title>

View File

@@ -5,16 +5,12 @@ import {
import { IntlProvider } from '@edx/frontend-platform/i18n';
import { useSelector } from 'react-redux';
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 initializeStore from '../../store';
import HighlightsModal from './HighlightsModal';
import messages from './messages';
// eslint-disable-next-line no-unused-vars
let axiosMock;
let store;
const mockPathname = '/foo-bar';
@@ -68,7 +64,6 @@ describe('<HighlightsModal />', () => {
});
store = initializeStore();
axiosMock = new MockAdapter(getAuthenticatedHttpClient());
useSelector.mockReturnValue(currentItemMock);
});

View File

@@ -183,17 +183,17 @@ const useCourseOutline = ({ courseId }) => {
const handleConfigureItemSubmit = (...arg) => {
switch (currentItem.category) {
case COURSE_BLOCK_NAMES.chapter.id:
dispatch(configureCourseSectionQuery(currentSection.id, ...arg));
break;
case COURSE_BLOCK_NAMES.sequential.id:
dispatch(configureCourseSubsectionQuery(currentItem.id, currentSection.id, ...arg));
break;
case COURSE_BLOCK_NAMES.vertical.id:
dispatch(configureCourseUnitQuery(currentItem.id, currentSection.id, ...arg));
break;
default:
return;
case COURSE_BLOCK_NAMES.chapter.id:
dispatch(configureCourseSectionQuery(currentSection.id, ...arg));
break;
case COURSE_BLOCK_NAMES.sequential.id:
dispatch(configureCourseSubsectionQuery(currentItem.id, currentSection.id, ...arg));
break;
case COURSE_BLOCK_NAMES.vertical.id:
dispatch(configureCourseUnitQuery(currentItem.id, currentSection.id, ...arg));
break;
default:
return;
}
handleConfigureModalClose();
};
@@ -204,21 +204,21 @@ const useCourseOutline = ({ courseId }) => {
const handleDeleteItemSubmit = () => {
switch (currentItem.category) {
case COURSE_BLOCK_NAMES.chapter.id:
dispatch(deleteCourseSectionQuery(currentItem.id));
break;
case COURSE_BLOCK_NAMES.sequential.id:
dispatch(deleteCourseSubsectionQuery(currentItem.id, currentSection.id));
break;
case COURSE_BLOCK_NAMES.vertical.id:
dispatch(deleteCourseUnitQuery(
currentItem.id,
currentSubsection.id,
currentSection.id,
));
break;
default:
return;
case COURSE_BLOCK_NAMES.chapter.id:
dispatch(deleteCourseSectionQuery(currentItem.id));
break;
case COURSE_BLOCK_NAMES.sequential.id:
dispatch(deleteCourseSubsectionQuery(currentItem.id, currentSection.id));
break;
case COURSE_BLOCK_NAMES.vertical.id:
dispatch(deleteCourseUnitQuery(
currentItem.id,
currentSubsection.id,
currentSection.id,
));
break;
default:
return;
}
closeDeleteModal();
};

View File

@@ -29,6 +29,21 @@ const messages = defineMessages({
id: 'course-authoring.course-outline.section-list.button.new-section',
defaultMessage: 'New section',
},
exportTagsCreatingToastMessage: {
id: 'course-authoring.course-outline.export-tags.toast.creating.message',
defaultMessage: 'Please wait. Creating export file for course tags...',
description: 'In progress message in toast when exporting tags of a course',
},
exportTagsSuccessToastMessage: {
id: 'course-authoring.course-outline.export-tags.toast.success.message',
defaultMessage: 'Course tags exported successfully',
description: 'Success message in toast when exporting tags of a course',
},
exportTagsErrorToastMessage: {
id: 'course-authoring.course-outline.export-tags.toast.error.message',
defaultMessage: 'An error has occurred creating the file',
description: 'Error message in toast when exporting tags of a course',
},
});
export default messages;

View File

@@ -1,9 +1,9 @@
import React, { useState } from 'react';
import PropTypes from 'prop-types';
import { uniqBy } from 'lodash';
import { getConfig } from '@edx/frontend-platform';
import { useDispatch, useSelector } from 'react-redux';
import { FormattedMessage, useIntl } from '@edx/frontend-platform/i18n';
import { ErrorAlert } from '@edx/frontend-lib-content-components';
import {
Campaign as CampaignIcon,
InfoOutline as InfoOutlineIcon,
@@ -15,6 +15,7 @@ import {
} from '@openedx/paragon';
import { Link } from 'react-router-dom';
import ErrorAlert from '../../editors/sharedComponents/ErrorAlerts/ErrorAlert';
import { RequestStatus } from '../../data/constants';
import AlertMessage from '../../generic/alert-message';
import AlertProctoringError from '../../generic/AlertProctoringError';
@@ -41,8 +42,11 @@ const PageAlerts = ({
const intl = useIntl();
const dispatch = useDispatch();
const studioBaseUrl = getConfig().STUDIO_BASE_URL;
const discussionAlertDismissKey = `discussionAlertDismissed-${courseId}`;
const [showConfigAlert, setShowConfigAlert] = useState(true);
const [showDiscussionAlert, setShowDiscussionAlert] = useState(true);
const [showDiscussionAlert, setShowDiscussionAlert] = useState(
localStorage.getItem(discussionAlertDismissKey) === null,
);
const { newFiles, conflictingFiles, errorFiles } = useSelector(getPasteFileNotices);
const getAssetsUrl = () => {
@@ -83,6 +87,7 @@ const PageAlerts = ({
const onDismiss = () => {
setShowDiscussionAlert(false);
localStorage.setItem(discussionAlertDismissKey, 'true');
};
return (
@@ -336,31 +341,30 @@ const PageAlerts = ({
};
const renderApiErrors = () => {
const errorList = Object.entries(errors).filter(obj => obj[1] !== null).map(([k, v]) => {
let errorList = Object.entries(errors).filter(obj => obj[1] !== null).map(([k, v]) => {
switch (v.type) {
case API_ERROR_TYPES.serverError:
return {
key: k,
desc: v.data,
title: intl.formatMessage(messages.serverErrorAlert, {
status: v.status,
}),
dismissible: v.dismissible,
};
case API_ERROR_TYPES.networkError:
return {
key: k,
title: intl.formatMessage(messages.networkErrorAlert),
dismissible: v.dismissible,
};
default:
return {
key: k,
desc: v.data,
dismissible: v.dismissible,
};
case API_ERROR_TYPES.serverError:
return {
key: k,
desc: v.data || intl.formatMessage(messages.serverErrorAlertBody),
title: intl.formatMessage(messages.serverErrorAlert),
dismissible: v.dismissible,
};
case API_ERROR_TYPES.networkError:
return {
key: k,
title: intl.formatMessage(messages.networkErrorAlert),
dismissible: v.dismissible,
};
default:
return {
key: k,
title: v.data,
dismissible: v.dismissible,
};
}
});
errorList = uniqBy(errorList, 'title');
if (!errorList?.length) {
return null;
}
@@ -373,10 +377,7 @@ const PageAlerts = ({
key={msgObj.key}
dismissError={() => dispatch(dismissError(msgObj.key))}
>
{msgObj.title
&& (
<Alert.Heading>{msgObj.title}</Alert.Heading>
)}
<Alert.Heading>{msgObj.title}</Alert.Heading>
{msgObj.desc && <Truncate lines={2}>{msgObj.desc}</Truncate>}
</ErrorAlert>
) : (
@@ -385,10 +386,7 @@ const PageAlerts = ({
icon={ErrorIcon}
key={msgObj.key}
>
{msgObj.title
&& (
<Alert.Heading>{msgObj.title}</Alert.Heading>
)}
<Alert.Heading>{msgObj.title}</Alert.Heading>
{msgObj.desc && <Truncate lines={2}>{msgObj.desc}</Truncate>}
</Alert>
)

View File

@@ -1,6 +1,12 @@
import React from 'react';
import { useSelector } from 'react-redux';
import { act, render, fireEvent } from '@testing-library/react';
import {
act,
render,
fireEvent,
screen,
waitFor,
} from '@testing-library/react';
import { IntlProvider } from '@edx/frontend-platform/i18n';
import { AppProvider } from '@edx/frontend-platform/react';
import { initializeMockApp, getConfig } from '@edx/frontend-platform';
@@ -84,7 +90,7 @@ describe('<PageAlerts />', () => {
});
it('renders discussion alerts', async () => {
const { queryByText } = renderComponent({
renderComponent({
...pageAlertsData,
discussionsSettings: {
providerType: 'openedx',
@@ -93,14 +99,21 @@ describe('<PageAlerts />', () => {
discussionsIncontextLearnmoreUrl: 'some-learn-more-url',
});
expect(queryByText(messages.discussionNotificationText.defaultMessage)).toBeInTheDocument();
const learnMoreBtn = queryByText(messages.discussionNotificationLearnMore.defaultMessage);
expect(screen.queryByText(messages.discussionNotificationText.defaultMessage)).toBeInTheDocument();
const learnMoreBtn = screen.queryByText(messages.discussionNotificationLearnMore.defaultMessage);
expect(learnMoreBtn).toBeInTheDocument();
expect(learnMoreBtn).toHaveAttribute('href', 'some-learn-more-url');
const feedbackLink = queryByText(messages.discussionNotificationFeedback.defaultMessage);
expect(feedbackLink).toBeInTheDocument();
expect(feedbackLink).toHaveAttribute('href', 'some-feedback-url');
const dismissBtn = screen.queryByText('Dismiss');
fireEvent.click(dismissBtn);
const discussionAlertDismissKey = `discussionAlertDismissed-${pageAlertsData.courseId}`;
expect(localStorage.getItem(discussionAlertDismissKey)).toBe('true');
await waitFor(() => {
const feedbackLink = screen.queryByText(messages.discussionNotificationFeedback.defaultMessage);
expect(feedbackLink).toBeInTheDocument();
expect(feedbackLink).toHaveAttribute('href', 'some-feedback-url');
});
});
it('renders deprecation warning alerts', async () => {

View File

@@ -22,7 +22,7 @@ const messages = defineMessages({
description: 'Learn more link in upgraded discussion notification alert',
},
discussionNotificationFeedback: {
id: 'course-authoring.course-outline.page-alerts.discussionNotificationLearnMore',
id: 'course-authoring.course-outline.page-alerts.discussionNotificationFeedback',
defaultMessage: 'Share feedback',
description: 'Share feedback link in upgraded discussion notification alert',
},
@@ -108,7 +108,12 @@ const messages = defineMessages({
},
serverErrorAlert: {
id: 'course-authoring.course-outline.page-alert.server-error.title',
defaultMessage: 'Request failed with status: {status}',
defaultMessage: 'The Studio servers encountered an error',
description: 'Generic server error alert title.',
},
serverErrorAlertBody: {
id: 'course-authoring.course-outline.page-alert.server-error.body',
defaultMessage: ' An error occurred in Studio and the page could not be loaded. Please try again in a few moments. We\'ve logged the error and our staff is currently working to resolve this error as soon as possible.',
description: 'Generic server error alert title.',
},
networkErrorAlert: {

View File

@@ -30,6 +30,7 @@ const PublishModal = ({
onClose={onClose}
hasCloseButton
isFullscreenOnMobile
isOverflowVisible={false}
>
<ModalDialog.Header className="publish-modal__header">
<ModalDialog.Title>

View File

@@ -3,16 +3,12 @@ import { render, fireEvent } from '@testing-library/react';
import { IntlProvider } from '@edx/frontend-platform/i18n';
import { useSelector } from 'react-redux';
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 initializeStore from '../../store';
import PublishModal from './PublishModal';
import messages from './messages';
// eslint-disable-next-line no-unused-vars
let axiosMock;
let store;
jest.mock('react-redux', () => ({
@@ -100,7 +96,6 @@ describe('<PublishModal />', () => {
});
store = initializeStore();
axiosMock = new MockAdapter(getAuthenticatedHttpClient());
useSelector.mockReturnValue(currentItemMock);
});

View File

@@ -6,15 +6,11 @@ import {
import { IntlProvider } from '@edx/frontend-platform/i18n';
import { AppProvider } from '@edx/frontend-platform/react';
import { initializeMockApp } from '@edx/frontend-platform';
import MockAdapter from 'axios-mock-adapter';
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import initializeStore from '../../store';
import SectionCard from './SectionCard';
// eslint-disable-next-line no-unused-vars
let axiosMock;
let store;
const mockPathname = '/foo-bar';
@@ -125,7 +121,6 @@ describe('<SectionCard />', () => {
});
store = initializeStore();
axiosMock = new MockAdapter(getAuthenticatedHttpClient());
});
it('render SectionCard component correctly', () => {

View File

@@ -56,7 +56,7 @@ const messages = defineMessages({
defaultMessage: 'Video Sharing',
},
videoSharingLink: {
id: 'course-authoring.course-outline.status-bar.video-sharing.title',
id: 'course-authoring.course-outline.status-bar.video-sharing.link',
defaultMessage: 'Learn more',
},
videoSharingPerVideoText: {

View File

@@ -5,16 +5,12 @@ import {
import { IntlProvider } from '@edx/frontend-platform/i18n';
import { AppProvider } from '@edx/frontend-platform/react';
import { initializeMockApp } from '@edx/frontend-platform';
import MockAdapter from 'axios-mock-adapter';
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import initializeStore from '../../store';
import SubsectionCard from './SubsectionCard';
import cardHeaderMessages from '../card-header/messages';
// eslint-disable-next-line no-unused-vars
let axiosMock;
let store;
const mockPathname = '/foo-bar';
@@ -121,7 +117,6 @@ describe('<SubsectionCard />', () => {
});
store = initializeStore();
axiosMock = new MockAdapter(getAuthenticatedHttpClient());
});
it('render SubsectionCard component correctly', () => {

View File

@@ -6,7 +6,7 @@ const messages = defineMessages({
defaultMessage: 'New unit',
},
pasteButton: {
id: 'course-authoring.course-outline.subsection.button.new-unit',
id: 'course-authoring.course-outline.subsection.button.paste-unit',
defaultMessage: 'Paste unit',
},
});

View File

@@ -5,16 +5,12 @@ import {
import { IntlProvider } from '@edx/frontend-platform/i18n';
import { AppProvider } from '@edx/frontend-platform/react';
import { initializeMockApp } from '@edx/frontend-platform';
import MockAdapter from 'axios-mock-adapter';
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import initializeStore from '../../store';
import UnitCard from './UnitCard';
import cardMessages from '../card-header/messages';
// eslint-disable-next-line no-unused-vars
let axiosMock;
let store;
const section = {
@@ -92,7 +88,6 @@ describe('<UnitCard />', () => {
});
store = initializeStore();
axiosMock = new MockAdapter(getAuthenticatedHttpClient());
});
it('render UnitCard component correctly', async () => {

View File

@@ -19,20 +19,20 @@ const getItemStatus = ({
hasChanges,
}) => {
switch (true) {
case visibilityState === VisibilityTypes.STAFF_ONLY:
return ITEM_BADGE_STATUS.staffOnly;
case visibilityState === VisibilityTypes.GATED:
return ITEM_BADGE_STATUS.gated;
case visibilityState === VisibilityTypes.LIVE:
return ITEM_BADGE_STATUS.live;
case visibilityState === VisibilityTypes.UNSCHEDULED:
return ITEM_BADGE_STATUS.unscheduled;
case published && !hasChanges:
return ITEM_BADGE_STATUS.publishedNotLive;
case published && hasChanges:
return ITEM_BADGE_STATUS.unpublishedChanges;
default:
return ITEM_BADGE_STATUS.draft;
case visibilityState === VisibilityTypes.STAFF_ONLY:
return ITEM_BADGE_STATUS.staffOnly;
case visibilityState === VisibilityTypes.GATED:
return ITEM_BADGE_STATUS.gated;
case visibilityState === VisibilityTypes.LIVE:
return ITEM_BADGE_STATUS.live;
case visibilityState === VisibilityTypes.UNSCHEDULED:
return ITEM_BADGE_STATUS.unscheduled;
case published && !hasChanges:
return ITEM_BADGE_STATUS.publishedNotLive;
case published && hasChanges:
return ITEM_BADGE_STATUS.unpublishedChanges;
default:
return ITEM_BADGE_STATUS.draft;
}
};
@@ -46,41 +46,41 @@ const getItemStatus = ({
*/
const getItemStatusBadgeContent = (status, messages, intl) => {
switch (status) {
case ITEM_BADGE_STATUS.gated:
return {
badgeTitle: intl.formatMessage(messages.statusBadgeGated),
badgeIcon: LockIcon,
};
case ITEM_BADGE_STATUS.live:
return {
badgeTitle: intl.formatMessage(messages.statusBadgeLive),
badgeIcon: CheckCircleIcon,
};
case ITEM_BADGE_STATUS.publishedNotLive:
return {
badgeTitle: intl.formatMessage(messages.statusBadgePublishedNotLive),
badgeIcon: null,
};
case ITEM_BADGE_STATUS.staffOnly:
return {
badgeTitle: intl.formatMessage(messages.statusBadgeStaffOnly),
badgeIcon: LockIcon,
};
case ITEM_BADGE_STATUS.unpublishedChanges:
return {
badgeTitle: intl.formatMessage(messages.statusBadgeUnpublishedChanges),
badgeIcon: DraftIcon,
};
case ITEM_BADGE_STATUS.draft:
return {
badgeTitle: intl.formatMessage(messages.statusBadgeDraft),
badgeIcon: DraftIcon,
};
default:
return {
badgeTitle: '',
badgeIcon: null,
};
case ITEM_BADGE_STATUS.gated:
return {
badgeTitle: intl.formatMessage(messages.statusBadgeGated),
badgeIcon: LockIcon,
};
case ITEM_BADGE_STATUS.live:
return {
badgeTitle: intl.formatMessage(messages.statusBadgeLive),
badgeIcon: CheckCircleIcon,
};
case ITEM_BADGE_STATUS.publishedNotLive:
return {
badgeTitle: intl.formatMessage(messages.statusBadgePublishedNotLive),
badgeIcon: null,
};
case ITEM_BADGE_STATUS.staffOnly:
return {
badgeTitle: intl.formatMessage(messages.statusBadgeStaffOnly),
badgeIcon: LockIcon,
};
case ITEM_BADGE_STATUS.unpublishedChanges:
return {
badgeTitle: intl.formatMessage(messages.statusBadgeUnpublishedChanges),
badgeIcon: DraftIcon,
};
case ITEM_BADGE_STATUS.draft:
return {
badgeTitle: intl.formatMessage(messages.statusBadgeDraft),
badgeIcon: DraftIcon,
};
default:
return {
badgeTitle: '',
badgeIcon: null,
};
}
};
@@ -93,36 +93,36 @@ const getItemStatusBadgeContent = (status, messages, intl) => {
*/
const getItemStatusBorder = (status) => {
switch (status) {
case ITEM_BADGE_STATUS.live:
return {
borderLeft: '5px solid #00688D',
};
case ITEM_BADGE_STATUS.publishedNotLive:
return {
borderLeft: '5px solid #0D7D4D',
};
case ITEM_BADGE_STATUS.gated:
return {
borderLeft: '5px solid #000000',
};
case ITEM_BADGE_STATUS.staffOnly:
return {
borderLeft: '5px solid #000000',
};
case ITEM_BADGE_STATUS.unpublishedChanges:
return {
borderLeft: '5px solid #F0CC00',
};
case ITEM_BADGE_STATUS.draft:
return {
borderLeft: '5px solid #F0CC00',
};
case ITEM_BADGE_STATUS.unscheduled:
return {
borderLeft: '5px solid #ccc',
};
default:
return {};
case ITEM_BADGE_STATUS.live:
return {
borderLeft: '5px solid #00688D',
};
case ITEM_BADGE_STATUS.publishedNotLive:
return {
borderLeft: '5px solid #0D7D4D',
};
case ITEM_BADGE_STATUS.gated:
return {
borderLeft: '5px solid #000000',
};
case ITEM_BADGE_STATUS.staffOnly:
return {
borderLeft: '5px solid #000000',
};
case ITEM_BADGE_STATUS.unpublishedChanges:
return {
borderLeft: '5px solid #F0CC00',
};
case ITEM_BADGE_STATUS.draft:
return {
borderLeft: '5px solid #F0CC00',
};
case ITEM_BADGE_STATUS.unscheduled:
return {
borderLeft: '5px solid #ccc',
};
default:
return {};
}
};
@@ -195,14 +195,14 @@ const scrollToElement = (target, alignWithTop = false) => {
*/
const getVideoSharingOptionText = (id, messages, intl) => {
switch (id) {
case VIDEO_SHARING_OPTIONS.perVideo:
return intl.formatMessage(messages.videoSharingPerVideoText);
case VIDEO_SHARING_OPTIONS.allOn:
return intl.formatMessage(messages.videoSharingAllOnText);
case VIDEO_SHARING_OPTIONS.allOff:
return intl.formatMessage(messages.videoSharingAllOffText);
default:
return '';
case VIDEO_SHARING_OPTIONS.perVideo:
return intl.formatMessage(messages.videoSharingPerVideoText);
case VIDEO_SHARING_OPTIONS.allOn:
return intl.formatMessage(messages.videoSharingAllOnText);
case VIDEO_SHARING_OPTIONS.allOff:
return intl.formatMessage(messages.videoSharingAllOffText);
default:
return '';
}
};

View File

@@ -6,14 +6,14 @@
export const hasWelcomeMessage = (updates) => updates.hasUpdate;
export const hasGradingPolicy = (grades) => {
// eslint-disable-next-line no-shadow
// eslint-disable-next-line @typescript-eslint/no-shadow
const { hasGradingPolicy, sumOfWeights } = grades;
return hasGradingPolicy && parseFloat(sumOfWeights.toPrecision(2), 10) === 1.0;
};
export const hasCertificate = (certificates) => {
// eslint-disable-next-line no-shadow
// eslint-disable-next-line @typescript-eslint/no-shadow
const { isActivated, hasCertificate } = certificates;
return isActivated && hasCertificate;

View File

@@ -20,30 +20,30 @@ const getChecklistValidatedValue = (data, id) => {
} = data;
switch (id) {
case 'welcomeMessage':
return healthValidators.hasWelcomeMessage(updates);
case 'gradingPolicy':
return healthValidators.hasGradingPolicy(grades);
case 'certificate':
return healthValidators.hasCertificate(certificates);
case 'courseDates':
return healthValidators.hasDates(dates);
case 'assignmentDeadlines':
return healthValidators.hasAssignmentDeadlines(assignments, dates);
case 'videoDuration':
return healthValidators.hasShortVideoDuration(videos);
case 'mobileFriendlyVideo':
return healthValidators.hasMobileFriendlyVideos(videos);
case 'diverseSequences':
return healthValidators.hasDiverseSequences(subsections);
case 'weeklyHighlights':
return healthValidators.hasWeeklyHighlights(sections);
case 'unitDepth':
return healthValidators.hasShortUnitDepth(units);
case 'proctoringEmail':
return healthValidators.hasProctoringEscalationEmail(proctoring);
default:
throw new Error(`Unknown validator ${id}.`);
case 'welcomeMessage':
return healthValidators.hasWelcomeMessage(updates);
case 'gradingPolicy':
return healthValidators.hasGradingPolicy(grades);
case 'certificate':
return healthValidators.hasCertificate(certificates);
case 'courseDates':
return healthValidators.hasDates(dates);
case 'assignmentDeadlines':
return healthValidators.hasAssignmentDeadlines(assignments, dates);
case 'videoDuration':
return healthValidators.hasShortVideoDuration(videos);
case 'mobileFriendlyVideo':
return healthValidators.hasMobileFriendlyVideos(videos);
case 'diverseSequences':
return healthValidators.hasDiverseSequences(subsections);
case 'weeklyHighlights':
return healthValidators.hasWeeklyHighlights(sections);
case 'unitDepth':
return healthValidators.hasShortUnitDepth(units);
case 'proctoringEmail':
return healthValidators.hasProctoringEscalationEmail(proctoring);
default:
throw new Error(`Unknown validator ${id}.`);
}
};

View File

@@ -3,15 +3,11 @@ import { render } from '@testing-library/react';
import { IntlProvider } from '@edx/frontend-platform/i18n';
import { AppProvider } from '@edx/frontend-platform/react';
import { initializeMockApp } from '@edx/frontend-platform';
import MockAdapter from 'axios-mock-adapter';
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
import initializeStore from '../../store';
import XBlockStatus from './XBlockStatus';
import messages from './messages';
// eslint-disable-next-line no-unused-vars
let axiosMock;
let store;
jest.mock('@edx/frontend-platform/i18n', () => ({
@@ -73,7 +69,6 @@ describe('<XBlockStatus /> for Instructor paced Section', () => {
});
store = initializeStore();
axiosMock = new MockAdapter(getAuthenticatedHttpClient());
});
it('render XBlockStatus with explanatoryMessage', () => {
@@ -141,7 +136,6 @@ describe('<XBlockStatus /> for self paced Section', () => {
});
store = initializeStore();
axiosMock = new MockAdapter(getAuthenticatedHttpClient());
});
it('renders XBlockStatus with grading type, due weeks etc.', () => {
@@ -245,7 +239,6 @@ describe('<XBlockStatus /> for Instructor paced Subsection', () => {
});
store = initializeStore();
axiosMock = new MockAdapter(getAuthenticatedHttpClient());
});
it('renders XBlockStatus with release status, grading type, due date etc.', () => {
@@ -375,7 +368,6 @@ describe('<XBlockStatus /> for self paced Subsection', () => {
});
store = initializeStore();
axiosMock = new MockAdapter(getAuthenticatedHttpClient());
});
it('renders XBlockStatus with grading type, due weeks etc.', () => {
@@ -456,7 +448,6 @@ describe('<XBlockStatus /> for unit', () => {
});
store = initializeStore();
axiosMock = new MockAdapter(getAuthenticatedHttpClient());
});
it('renders XBlockStatus with status messages', () => {

View File

@@ -1,13 +1,11 @@
import { useEffect } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { useIntl } from '@edx/frontend-platform/i18n';
import { useNavigate } from 'react-router-dom';
import { RequestStatus } from '../data/constants';
import { updateSavingStatus } from '../generic/data/slice';
import {
getSavingStatus,
getRedirectUrlObj,
getCourseRerunData,
getCourseData,
} from '../generic/data/selectors';
@@ -17,11 +15,9 @@ import { fetchStudioHomeData } from '../studio-home/data/thunks';
const useCourseRerun = (courseId) => {
const intl = useIntl();
const dispatch = useDispatch();
const navigate = useNavigate();
const savingStatus = useSelector(getSavingStatus);
const courseData = useSelector(getCourseData);
const courseRerunData = useSelector(getCourseRerunData);
const redirectUrlObj = useSelector(getRedirectUrlObj);
const {
displayName = '',
@@ -46,10 +42,6 @@ const useCourseRerun = (courseId) => {
useEffect(() => {
if (savingStatus === RequestStatus.SUCCESSFUL) {
dispatch(updateSavingStatus({ status: '' }));
const { url } = redirectUrlObj;
if (url) {
navigate('/home');
}
}
}, [savingStatus]);

View File

@@ -22,7 +22,7 @@ const messages = defineMessages({
defaultMessage: 'Add admin access',
},
removeButton: {
id: 'course-authoring.course-team.member.button.remove',
id: 'course-authoring.course-team.member.button.remove-admin-access',
defaultMessage: 'Remove admin access',
},
deleteUserButton: {

View File

@@ -19,33 +19,33 @@ import messages from './info-modal/messages';
const getInfoModalSettings = (modalType, currentEmail, errorMessage, courseName, intl) => {
switch (modalType) {
case MODAL_TYPES.delete:
return {
title: intl.formatMessage(messages.deleteModalTitle),
message: intl.formatMessage(messages.deleteModalMessage, { email: currentEmail, courseName }),
variant: '',
closeButtonText: intl.formatMessage(messages.deleteModalCancelButton),
submitButtonText: intl.formatMessage(messages.deleteModalDeleteButton),
closeButtonVariant: 'tertiary',
};
case MODAL_TYPES.error:
return {
title: intl.formatMessage(messages.errorModalTitle),
message: errorMessage,
variant: 'danger',
closeButtonText: intl.formatMessage(messages.errorModalOkButton),
closeButtonVariant: 'primary',
};
case MODAL_TYPES.warning:
return {
title: intl.formatMessage(messages.warningModalTitle),
message: intl.formatMessage(messages.warningModalMessage, { email: currentEmail, courseName }),
variant: 'warning',
closeButtonText: intl.formatMessage(messages.warningModalReturnButton),
mainButtonVariant: 'primary',
};
default:
return '';
case MODAL_TYPES.delete:
return {
title: intl.formatMessage(messages.deleteModalTitle),
message: intl.formatMessage(messages.deleteModalMessage, { email: currentEmail, courseName }),
variant: '',
closeButtonText: intl.formatMessage(messages.deleteModalCancelButton),
submitButtonText: intl.formatMessage(messages.deleteModalDeleteButton),
closeButtonVariant: 'tertiary',
};
case MODAL_TYPES.error:
return {
title: intl.formatMessage(messages.errorModalTitle),
message: errorMessage,
variant: 'danger',
closeButtonText: intl.formatMessage(messages.errorModalOkButton),
closeButtonVariant: 'primary',
};
case MODAL_TYPES.warning:
return {
title: intl.formatMessage(messages.warningModalTitle),
message: intl.formatMessage(messages.warningModalMessage, { email: currentEmail, courseName }),
variant: 'warning',
closeButtonText: intl.formatMessage(messages.warningModalReturnButton),
mainButtonVariant: 'primary',
};
default:
return '';
}
};

View File

@@ -7,8 +7,8 @@ import { getConfig } from '@edx/frontend-platform';
import { useIntl, injectIntl } from '@edx/frontend-platform/i18n';
import { Warning as WarningIcon } from '@openedx/paragon/icons';
import { SortableContext, verticalListSortingStrategy } from '@dnd-kit/sortable';
import { DraggableList } from '@edx/frontend-lib-content-components';
import DraggableList from '../editors/sharedComponents/DraggableList';
import { getProcessingNotification } from '../generic/processing-notification/data/selectors';
import SubHeader from '../generic/sub-header/SubHeader';
import { RequestStatus } from '../data/constants';

View File

@@ -1,6 +1,6 @@
import MockAdapter from 'axios-mock-adapter';
import {
act, render, waitFor, fireEvent, within,
act, render, waitFor, fireEvent, within, screen,
} from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { IntlProvider } from '@edx/frontend-platform/i18n';
@@ -525,17 +525,19 @@ describe('<CourseUnit />', () => {
});
it('should display a warning alert for unpublished course unit version', async () => {
const { getByRole } = render(<RootWrapper />);
render(<RootWrapper />);
await waitFor(() => {
const unpublishedAlert = getByRole('alert', { class: 'course-unit-unpublished-alert' });
const unpublishedAlert = screen.getAllByRole('alert').find(
(el) => el.classList.contains('alert-content'),
);
expect(unpublishedAlert).toHaveTextContent(messages.alertUnpublishedVersion.defaultMessage);
expect(unpublishedAlert).toHaveClass('alert-warning');
});
});
it('should not display an unpublished alert for a course unit with explicit staff lock and unpublished status', async () => {
const { queryByRole } = render(<RootWrapper />);
render(<RootWrapper />);
axiosMock
.onGet(getCourseUnitApiUrl(courseId))
@@ -547,8 +549,10 @@ describe('<CourseUnit />', () => {
await executeThunk(fetchCourseUnitQuery(courseId), store.dispatch);
await waitFor(() => {
const unpublishedAlert = queryByRole('alert', { class: 'course-unit-unpublished-alert' });
expect(unpublishedAlert).toBeNull();
const alert = screen.queryAllByRole('alert').find(
(el) => el.classList.contains('alert-content'),
);
expect(alert).toBeUndefined();
});
});

View File

@@ -5,7 +5,7 @@ import { useIntl } from '@edx/frontend-platform/i18n';
import { useToggle } from '@openedx/paragon';
import { getCourseSectionVertical } from '../data/selectors';
import { COMPONENT_TYPES } from '../constants';
import { COMPONENT_TYPES } from '../../generic/block-type-utils/constants';
import ComponentModalView from './add-component-modals/ComponentModalView';
import AddComponentButton from './add-component-btn';
import messages from './messages';
@@ -20,41 +20,41 @@ const AddComponent = ({ blockId, handleCreateNewCourseXBlock }) => {
const handleCreateNewXBlock = (type, moduleName) => {
switch (type) {
case COMPONENT_TYPES.discussion:
case COMPONENT_TYPES.dragAndDrop:
handleCreateNewCourseXBlock({ type, parentLocator: blockId });
break;
case COMPONENT_TYPES.problem:
case COMPONENT_TYPES.video:
handleCreateNewCourseXBlock({ type, parentLocator: blockId }, ({ courseKey, locator }) => {
navigate(`/course/${courseKey}/editor/${type}/${locator}`);
});
break;
// TODO: The library functional will be a bit different of current legacy (CMS)
// behaviour and this ticket is on hold (blocked by other development team).
case COMPONENT_TYPES.library:
handleCreateNewCourseXBlock({ type, category: 'library_content', parentLocator: blockId });
break;
case COMPONENT_TYPES.advanced:
handleCreateNewCourseXBlock({
type: moduleName, category: moduleName, parentLocator: blockId,
});
break;
case COMPONENT_TYPES.openassessment:
handleCreateNewCourseXBlock({
boilerplate: moduleName, category: type, parentLocator: blockId,
});
break;
case COMPONENT_TYPES.html:
handleCreateNewCourseXBlock({
type,
boilerplate: moduleName,
parentLocator: blockId,
}, ({ courseKey, locator }) => {
navigate(`/course/${courseKey}/editor/html/${locator}`);
});
break;
default:
case COMPONENT_TYPES.discussion:
case COMPONENT_TYPES.dragAndDrop:
handleCreateNewCourseXBlock({ type, parentLocator: blockId });
break;
case COMPONENT_TYPES.problem:
case COMPONENT_TYPES.video:
handleCreateNewCourseXBlock({ type, parentLocator: blockId }, ({ courseKey, locator }) => {
navigate(`/course/${courseKey}/editor/${type}/${locator}`);
});
break;
// TODO: The library functional will be a bit different of current legacy (CMS)
// behaviour and this ticket is on hold (blocked by other development team).
case COMPONENT_TYPES.library:
handleCreateNewCourseXBlock({ type, category: 'library_content', parentLocator: blockId });
break;
case COMPONENT_TYPES.advanced:
handleCreateNewCourseXBlock({
type: moduleName, category: moduleName, parentLocator: blockId,
});
break;
case COMPONENT_TYPES.openassessment:
handleCreateNewCourseXBlock({
boilerplate: moduleName, category: type, parentLocator: blockId,
});
break;
case COMPONENT_TYPES.html:
handleCreateNewCourseXBlock({
type,
boilerplate: moduleName,
parentLocator: blockId,
}, ({ courseKey, locator }) => {
navigate(`/course/${courseKey}/editor/html/${locator}`);
});
break;
default:
}
};
@@ -75,37 +75,37 @@ const AddComponent = ({ blockId, handleCreateNewCourseXBlock }) => {
}
switch (type) {
case COMPONENT_TYPES.advanced:
modalParams = {
open: openAdvanced,
close: closeAdvanced,
isOpen: isOpenAdvanced,
};
break;
case COMPONENT_TYPES.html:
modalParams = {
open: openHtml,
close: closeHtml,
isOpen: isOpenHtml,
};
break;
case COMPONENT_TYPES.openassessment:
modalParams = {
open: openOpenAssessment,
close: closeOpenAssessment,
isOpen: isOpenOpenAssessment,
};
break;
default:
return (
<li key={type}>
<AddComponentButton
onClick={() => handleCreateNewXBlock(type)}
displayName={displayName}
type={type}
/>
</li>
);
case COMPONENT_TYPES.advanced:
modalParams = {
open: openAdvanced,
close: closeAdvanced,
isOpen: isOpenAdvanced,
};
break;
case COMPONENT_TYPES.html:
modalParams = {
open: openHtml,
close: closeHtml,
isOpen: isOpenHtml,
};
break;
case COMPONENT_TYPES.openassessment:
modalParams = {
open: openOpenAssessment,
close: closeOpenAssessment,
isOpen: isOpenOpenAssessment,
};
break;
default:
return (
<li key={type}>
<AddComponentButton
onClick={() => handleCreateNewXBlock(type)}
displayName={displayName}
type={type}
/>
</li>
);
}
return (

View File

@@ -14,7 +14,7 @@ import { executeThunk } from '../../utils';
import { fetchCourseSectionVerticalData } from '../data/thunk';
import { getCourseSectionVerticalApiUrl } from '../data/api';
import { courseSectionVerticalMock } from '../__mocks__';
import { COMPONENT_TYPES } from '../constants';
import { COMPONENT_TYPES } from '../../generic/block-type-utils/constants';
import AddComponent from './AddComponent';
import messages from './messages';

View File

@@ -2,7 +2,7 @@ import PropTypes from 'prop-types';
import { Icon } from '@openedx/paragon';
import { EditNote as EditNoteIcon } from '@openedx/paragon/icons';
import { COMPONENT_TYPES, COMPONENT_TYPE_ICON_MAP } from '../../constants';
import { COMPONENT_TYPES, COMPONENT_TYPE_ICON_MAP } from '../../../generic/block-type-utils/constants';
const AddComponentIcon = ({ type }) => {
const icon = COMPONENT_TYPE_ICON_MAP[type] || EditNoteIcon;

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