Compare commits

...

658 Commits

Author SHA1 Message Date
dependabot[bot]
3c27558994 chore(deps): bump axios and @edx/frontend-platform
Bumps [axios](https://github.com/axios/axios) to 0.30.3 and updates ancestor dependencies [axios](https://github.com/axios/axios) and [@edx/frontend-platform](https://github.com/openedx/frontend-platform). These dependencies need to be updated together.


Updates `axios` from 0.30.2 to 0.30.3
- [Release notes](https://github.com/axios/axios/releases)
- [Changelog](https://github.com/axios/axios/blob/v1.x/CHANGELOG.md)
- [Commits](https://github.com/axios/axios/compare/v0.30.2...v0.30.3)

Updates `axios` from 1.13.4 to 1.13.5
- [Release notes](https://github.com/axios/axios/releases)
- [Changelog](https://github.com/axios/axios/blob/v1.x/CHANGELOG.md)
- [Commits](https://github.com/axios/axios/compare/v0.30.2...v0.30.3)

Updates `@edx/frontend-platform` from 8.5.4 to 8.5.5
- [Release notes](https://github.com/openedx/frontend-platform/releases)
- [Commits](https://github.com/openedx/frontend-platform/compare/v8.5.4...v8.5.5)

---
updated-dependencies:
- dependency-name: axios
  dependency-version: 0.30.3
  dependency-type: indirect
- dependency-name: axios
  dependency-version: 1.13.5
  dependency-type: indirect
- dependency-name: "@edx/frontend-platform"
  dependency-version: 8.5.5
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-02-18 23:20:01 +00:00
renovate[bot]
7a8b4ced01 chore(deps): update dependency fast-xml-parser to v5.3.6 [security] (#2886)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-18 23:18:47 +00:00
Muhammad Faraz Maqsood
e116e10845 fix: localize rerun button text (#2887)
* fix: localize rerun button text

* chore: use <FormattedMessage /> instead of formatMessage()

---------

Co-authored-by: Muhammad Faraz  Maqsood <faraz.maqsood@A006-01130.local>
Co-authored-by: Braden MacDonald <braden@opencraft.com>
2026-02-18 22:58:12 +00:00
MuPp3t33r
0b357a1be1 fix: Replace duplicate hardcoded max file size with constant (#2875) 2026-02-18 14:26:08 -08:00
edX requirements bot
e281681168 chore: update browserslist DB (#2881)
Co-authored-by: abdullahwaheed <42172960+abdullahwaheed@users.noreply.github.com>
2026-02-16 00:32:33 +00:00
Chris Chávez
c39923fc81 fix: Nits on UX/UI in the new sidebars in course outline and unit page [FC-0114] (#2871)
- Fixes the open nits/issues listed in https://github.com/openedx/frontend-app-authoring/issues/2868
- Fixes the small nit in the back button in the import workflow: https://github.com/openedx/frontend-app-authoring/issues/2524#issuecomment-3849649651
- Fixes the small nit in the unsupported text in the import workflow: https://github.com/openedx/frontend-app-authoring/issues/2525#issuecomment-3862737018. This fix depends on https://github.com/openedx/openedx-platform/pull/38005
2026-02-13 15:09:28 -05:00
Chris Chávez
4d401a9c22 feat: Unit align sidebar [FC-0114] (#2856)
Implements the align sidebar in the unit page for the unit and components
2026-02-13 15:07:53 -05:00
renovate[bot]
72421969d9 chore(deps): update dependency @tanstack/react-query to v5.90.21 (#2858)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-13 14:28:28 -05:00
renovate[bot]
0aad03420b chore(deps): update codemirror (#2857)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-13 19:28:19 +00:00
renovate[bot]
45811ce6de chore(deps): update dependency fast-xml-parser to v5.3.5 (#2867)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-13 11:14:30 -08:00
renovate[bot]
d1c3fb96de chore(deps): update dependency @types/react to v18.3.28 (#2866)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-09 10:38:36 -08:00
Chris Chávez
2d5421e09f feat: Unit Add sidebar [FC-0114] (#2837)
Implements the new Unit add Sidebar
2026-02-09 13:17:04 -05:00
Chris Chávez
a16087a1d0 fix: Nits on the style of library icon [FC-0114] (#2863) 2026-02-09 11:59:24 -05:00
Navin Karkera
bb6b2ab33c feat: container info sidebar and add sidebar updates (#2830)
* Adds section, subsection and unit sidebar info tab in course outline as described in https://github.com/openedx/frontend-app-authoring/issues/2638
* Updates the sidebar design and behaviour as per https://github.com/openedx/frontend-app-authoring/issues/2826
* Updates course outline to use react query and removes redux store usage as much as possible. Updated parts that require absolutely cannot work without redux without heavy refactoring (will require quiet some time) to work in tandem with react-query.
2026-02-09 11:03:27 -05:00
renovate[bot]
eae178a9ec chore(deps): update dependency lodash to v4.17.23 [security] (#2861)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-09 00:44:37 +00:00
edX requirements bot
2ab1219f36 chore: update browserslist DB (#2865)
Co-authored-by: abdullahwaheed <42172960+abdullahwaheed@users.noreply.github.com>
2026-02-09 00:28:05 +00:00
Rômulo Penido
7173b71cc4 fix: update sidebar design (#2852) 2026-02-05 15:44:46 -05:00
Brian Smith
f900ace15c chore(deps): regenerate package-lock.json (#2862)
Co-authored-by: Claude Code <noreply@anthropic.com>
2026-02-02 13:58:30 -08:00
Braden MacDonald
d44425c68f test: lint using oxlint as well as eslint (#2847)
PR enables the use of oxlint (type-aware) alongside eslint, on a trial basis. Oxlint supports most of the same rules, plus more, is eslint compatible, and is much, much faster.
2026-02-02 11:47:31 -05:00
Braden MacDonald
6effb4d39e chore: fix various lint/type issues found by oxlint (#2850) 2026-01-31 01:01:11 +00:00
Rômulo Penido
b0066e547c feat: create context for sidebarPages (#2827)
Creates the `OutlineSidebarPagesContext` so we can add new pages using plugins, as in https://github.com/openedx/frontend-plugin-aspects/pull/115
2026-01-30 17:52:52 -05:00
Rômulo Penido
6558c2b1ed fix: access restricted label refresh (#2846) 2026-01-30 17:09:42 -05:00
Braden MacDonald
747c2bc1b0 chore: clean up ValidationResult for NumericalInput problems (#2849) 2026-01-29 09:48:40 -08:00
Braden MacDonald
27e709912d fix: don't use eval() to parse OLX (#2848) 2026-01-29 10:39:48 -05:00
Braden MacDonald
9d9d7a7167 fix: resolve incorrect or missing 'await' usages (#2592)
* chore: fix incorrect or missing 'await' usages
* test: fix broken test
* chore: ignore lines causing patch coverage to fail
2026-01-28 16:48:13 -08:00
renovate[bot]
7e53e1143f chore(deps): update codemirror (#2840)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-28 16:32:53 -08:00
renovate[bot]
66656753b4 fix(deps): update dependency @edx/browserslist-config to v1.5.1 (#2844)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-28 10:41:07 -08:00
Irtaza Akram
05de984fa4 fix: show deafult grading method 2026-01-28 08:57:04 -05:00
Irtaza Akram
f6da58a950 fix: test 2026-01-28 08:57:04 -05:00
Irtaza Akram
5e5c92f487 fix: type error 2026-01-28 08:57:04 -05:00
Irtaza Akram
10705bc921 fix: test failures 2026-01-28 08:57:04 -05:00
Irtaza Akram
2e836a55cf fix: add grading method view 2026-01-28 08:57:04 -05:00
Braden MacDonald
292a457834 chore: rename more files from .js to .ts (#2842)
* chore: rename more files from .js to .ts

* fix: fix some little issues caught by TS

* fix: fix type of 'certificateId'
2026-01-27 13:16:47 -08:00
renovate[bot]
5d97a98294 chore(deps): update dependency @edx/frontend-platform to v8.5.4 (#2838)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-26 17:46:53 -08:00
renovate[bot]
53923d0db4 chore(deps): update dependency @testing-library/react to v16.3.2 (#2841)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-26 15:44:27 -08:00
Chris Chávez
5157cbcfb2 feat: New Unit info sidebar [FC-0114] (#2822)
- Implements the basics for the Unit Sidebar:
    - Splits the sidebar in legacy sidebar and in the new sidebar
- Implements the Unit Info Sidebar:
    - Implements a new design for the visibility and publish status card.
    - Implements the new Visibility field.
    - Implements the settings tab for the sidebar. Implements all the new form to edit the 
      settings in the sidebar.
2026-01-26 16:18:32 -05:00
Chris Chávez
ef93e95dd7 feat: New header in course unit page [FC-0114] (#2751)
- `ENABLE_UNIT_PAGE_NEW_DESIGN` flag created
- New Status Bard implemented in the header of the course unit page.
- New buttons added in the header of the course unit page.
- Which user roles will this change impact? "Course Author".
2026-01-26 12:52:50 -05:00
Braden MacDonald
1626d6808d chore: rename all 'messages.js' to 'messages.ts' (#2836) 2026-01-26 12:22:09 -05:00
edX requirements bot
b1c3151ca1 chore: update browserslist DB (#2839)
Co-authored-by: abdullahwaheed <42172960+abdullahwaheed@users.noreply.github.com>
2026-01-26 00:24:23 +00:00
renovate[bot]
bea1619537 chore(deps): update dependency @openedx/paragon to v23.19.1 (#2765)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-23 23:00:25 +00:00
Navin Karkera
89e327b633 feat: course outline header update [FC-0114] (#2823)
Modifies new course outline header and actions
2026-01-23 09:08:19 -05:00
Braden MacDonald
0db79f3527 fix: clean up invalid breadcrumb usages in library updates (#2811) 2026-01-22 09:45:34 -08:00
Chris Chávez
82e24193a8 refactor: Use preview migration API & feat: Block import when the import would exceed the block limit [FC-0112] (#2700)
- Use the new preview migration API implemented in https://github.com/openedx/edx-platform/pull/37818
- Clean the code for the preview.
- Implements the commented in https://github.com/openedx/frontend-app-authoring/issues/2525#issuecomment-3554310315
2026-01-20 14:44:35 -05:00
renovate[bot]
f57d33a74e chore(deps): update react-router monorepo to v6.30.3 (#2819)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-19 16:35:50 -08:00
Chris Chávez
0b2b8e142c feat: New align sidebar for Course Outline [FC-0114] (#2812)
- Adds the align sidebar for Course and Section/Subsection/Unit cards (https://github.com/openedx/frontend-app-authoring/issues/2625)
- Add a new library lock icon to tags imported from upstream (https://github.com/openedx/frontend-app-authoring/issues/2234)
2026-01-19 11:59:43 -05:00
edX requirements bot
d5352b8632 chore: update browserslist DB (#2828)
Co-authored-by: abdullahwaheed <42172960+abdullahwaheed@users.noreply.github.com>
2026-01-19 00:24:56 +00:00
Chris Chávez
3076dc7598 fix: Bug when adding spaces to containers names in course outline (#2825)
- Fixes the bug introduced in https://github.com/openedx/frontend-app-authoring/pull/2732
- Removes the unnecessary `preventDefault`
2026-01-15 09:12:35 -05:00
Navin Karkera
a23a4da0a2 feat: add content in location in course outline [FC-0114] (#2820)
- Add container in-location in course outline using new add sidebar.
- Creates the placeholder card while creating a container
2026-01-15 07:29:26 -05:00
Chris Chávez
4cda17e046 feat: Make sections/subsections/units selectable in course outline [FC-0114] (#2732) 2026-01-12 21:12:46 -05:00
renovate[bot]
969f7a2858 chore(deps): update dependency @types/lodash to v4.17.23 (#2818)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-12 09:47:14 -08:00
edX requirements bot
12f1c2b1f5 chore: update browserslist DB (#2817)
Co-authored-by: abdullahwaheed <42172960+abdullahwaheed@users.noreply.github.com>
2026-01-12 00:26:34 +00:00
Muhammad Anas
6464f37e2a feat: plugin slot for marketing banner on Schedule & Details page (#2748)
* feat: add Page Banner Slot for Schedule and Details Page

* fix: js to ts

* fix: remove js

* fix: lint issues

* fix: issues

* fix: lint issues

* fix: issues

* fix: issue

* fix: issue
2026-01-09 15:21:12 -08:00
renovate[bot]
5641daf68d chore(deps): update dependency react-responsive to v10 (#2784)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-09 11:16:35 -08:00
Navin Karkera
3c22e4bbe1 feat: Add sidebar and library dropdown filter [FC-0114] (#2778)
* Add flow in course outline sidebar. Allows author to add new section/subsection/unit or any container from existing libraries via sidebar.
* Adds library dropdown filter and collections dropdown filter in add sidebar. Allows authors to filter containers by selected libraries and collections.
2026-01-09 12:14:48 -05:00
renovate[bot]
a7cbfead75 chore(deps): update codemirror to v6.5.3 (#2771)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-09 00:52:38 +00:00
renovate[bot]
7b0223cefc chore(deps): update dependency @tanstack/react-query to v5.90.16 (#2781)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-08 16:39:20 -08:00
renovate[bot]
478749028b chore(deps): update dependency @testing-library/react to v16.3.1 (#2770)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-07 09:49:31 -08:00
Rômulo Penido
220924233e feat: course outline sidebar (#2731)
implements the new sidebar design for the Course Outline
2026-01-06 08:13:25 -05:00
edX requirements bot
122414cb73 chore: update browserslist DB (#2780)
Co-authored-by: abdullahwaheed <42172960+abdullahwaheed@users.noreply.github.com>
2026-01-05 00:25:41 +00:00
Rômulo Penido
fd0395ba03 fix: make sidebar (and content) fill the whole page (#2777) 2025-12-31 18:15:47 -05:00
Navin Karkera
38dfb68286 refactor: course import analysis and details page in libraries (#2774)
* Updates analysis details body text
* Updates partial banner text
* Rounds percentage of supported blocks
* Removes unsupported children blocks from total counts and percentages.
* Updates spacing in analysis page.
2025-12-31 10:36:44 -05:00
Navin Karkera
0f20267cc4 feat: course import filter in library and fix list order (#2773)
* Adds filter to show/hide previously imported courses in course import page in libraries.
* Fix issue with ordering of courses listing.
2025-12-29 09:37:44 -05:00
Rômulo Penido
7f10575b52 fix: import summary text (#2772)
Updates the text on the import summary shown on partial imports.
2025-12-24 12:28:31 -05:00
Chris Chávez
ef6bd073c0 refactor: Migrate accessibilityPage variable in Redux store to React Query (#2760) 2025-12-23 09:18:13 -05:00
Navin Karkera
f4d20eba45 feat: bulk update legacy library references (#2764)
Shows an alert in course outline and review tab of course libraries page when the course contains legacy library content blocks that depend on libraries that are already migrated to library v2, i.e. the blocks are ready to be converted into item banks that can make use of these new v2 libraries.
Authors can click on a single button to convert all references in a single go. The button launches a background task which is then polled by the frontend and the status is presented to the Author.
2025-12-22 12:54:54 -05:00
edX requirements bot
68a4b04475 chore: update browserslist DB (#2769)
Co-authored-by: abdullahwaheed <42172960+abdullahwaheed@users.noreply.github.com>
2025-12-22 00:24:49 +00:00
Diana Villalvazo
c2592a7e6e Add external links override support (#2730)
* feat: add external links override support

* fix: add note, fix 404 url and fix unrelated typo

* test: fix
2025-12-19 13:06:03 -06:00
Jesus Balderrama
9072bb66b7 feat: better validation for NumericalInput problem editor (#2615)
* feat(form): add validation to NumericalInput to accept only numeric values

* style(format): fix spaces and update message to camelCase

* fix(content): update text for clarity

Co-authored-by: Kyle McCormick <kyle@kylemccormick.me>

* feat(validation): validation added to numeric input with new endpoint to see if is a valid math expression

* fix(content): change in input validation to use react query instead of redux

* fix(content): change in types to avoid ci errors

* fix(content): remove unnecessary code after changing to react query

* fix(content): change numeric input validation path to new url and loader added

* feat: returning data in camelcase, improve UI in validation

* feat: tests added to problem editor

---------

Co-authored-by: Kyle McCormick <kyle@kylemccormick.me>
2025-12-19 10:38:19 -08:00
Areeb Sajjad
8b9f156149 fix: keyboard accessibility for time picker (#2613) 2025-12-17 16:56:54 -08:00
María Fernanda Magallanes
9c70fd9216 fix: only show library actions available for the user (#2712)
* fix: only show the options available for the user

* test: fix and add tests

* fix: improve following the best practices

* fix: apply the changes for collections and containers
2025-12-17 16:49:54 -08:00
Max Sokolski
4df44ab6cf fix: coerce multiple choice text elements to String (#2752) 2025-12-17 16:22:31 -08:00
renovate[bot]
0405156f91 chore(deps): update dependency @reduxjs/toolkit to v2.11.2 (#2755)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-17 23:55:12 +00:00
renovate[bot]
1e2e711eaf chore(deps): update dependency fast-xml-parser to v5.3.3 (#2754)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-17 15:42:14 -08:00
Navin Karkera
3eeca244d7 feat: add temporary message alert in sections settings tab in libraries (#2734)
- add temporary message alert in sections settings tab in libraries 
- increase sidebar width to remove `More` option and display all tabs
together
2025-12-17 14:21:13 -05:00
Navin Karkera
ae67be83a0 feat: new course outline header [FC-0114] (#2735)
Adds new header and subheader to course outline. Converts existing js code to ts.
2025-12-17 13:13:19 -05:00
Navin Karkera
6f37118960 refactor: update placeholder block and import details page (#2761)
* Updates placeholder block color and icon.
* Moves `View Imported Content` & `Retry import` buttons in import details page inside alert at the top.
* Updates page title to include Import status in import details page.
2025-12-17 11:43:12 -05:00
Chris Chávez
41a326f7b4 feat: New library sync icon [FC-0114] (#2739)
Updates the library sync icon with new states: error, broken, ready to sync and with overrides
2025-12-16 14:54:28 -05:00
dependabot[bot]
b7dd6706c2 chore(deps): bump actions/upload-artifact from 5 to 6 (#2756)
Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 5 to 6.
- [Release notes](https://github.com/actions/upload-artifact/releases)
- [Commits](https://github.com/actions/upload-artifact/compare/v5...v6)

---
updated-dependencies:
- dependency-name: actions/upload-artifact
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-15 17:59:54 +00:00
dependabot[bot]
64ab7ea4fe chore(deps): bump actions/download-artifact from 6 to 7 (#2757)
Bumps [actions/download-artifact](https://github.com/actions/download-artifact) from 6 to 7.
- [Release notes](https://github.com/actions/download-artifact/releases)
- [Commits](https://github.com/actions/download-artifact/compare/v6...v7)

---
updated-dependencies:
- dependency-name: actions/download-artifact
  dependency-version: '7'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-15 09:42:37 -08:00
edX requirements bot
89d2393d6e chore: update browserslist DB (#2753)
Co-authored-by: abdullahwaheed <42172960+abdullahwaheed@users.noreply.github.com>
2025-12-15 00:24:21 +00:00
Santhosh Kumar
42acb53201 feat!: remove "Create Zendesk Tickets for suspicious attempts" setting from Proctored Exam Settings (#2517)
BREAKING CHANGE: This PR removes the deprecated “Create Zendesk Tickets for suspicious attempts”
setting from the Proctored Exam Settings modal in the frontend-app-authoring
MFE. This option was previously used with PSI and Zendesk to generate support
tickets for suspicious exam attempts. Since both systems are retired, the
setting no longer serves a purpose and has been fully removed.

Part of: https://github.com/openedx/edx-platform/issues/36329
2025-12-11 17:05:05 -05:00
David Ormsbee
225c82d037 fix: support "in progress" status for lib upload
When uploading a library archive file during the creation of a new
library, the code prior to this commit did not properly handle the "In
Progress" state, which is when the celery task doing the archive
processing is actively running. Note that this is distinct from the
"Pending" state, which is when the task is waiting in the queue to be
run (which in practice should almost never happen unless there is an
operational issue).

Since celery tasks run in-process during local development, the task
was always finished by the time that the browser made a call to check
on the status. The problem only happened on slower sandboxes, where
processing truly runs asynchronously and might take a few seconds.
Because this case wasn't handled, the frontend would never poll for
updates either, so the upload was basically lost as far as the user
was concerned.
2025-12-11 12:58:06 -05:00
Muhammad Arslan
a596bf5d38 fix: bugs with some static asset paths in TinyMCE editors (#2702)
* fix: only convert assets static file path

* fix: direct static file is fixed

* fix: setting newContent now load the content in html as well
2025-12-11 09:19:40 -08:00
Kyle McCormick
67a5694ca3 fix: Generalize a couple remaining Proctortrack references (#2746)
Just a few leftovers from: https://github.com/openedx/frontend-app-authoring/pull/2645

Full context: https://github.com/openedx/edx-platform/issues/36329
2025-12-11 00:07:29 -05:00
dependabot[bot]
833a838348 chore(deps): bump node-forge from 1.3.1 to 1.3.2 (#2708)
Bumps [node-forge](https://github.com/digitalbazaar/forge) from 1.3.1 to 1.3.2.
- [Changelog](https://github.com/digitalbazaar/forge/blob/main/CHANGELOG.md)
- [Commits](https://github.com/digitalbazaar/forge/compare/v1.3.1...v1.3.2)

---
updated-dependencies:
- dependency-name: node-forge
  dependency-version: 1.3.2
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-10 15:21:18 -08:00
renovate[bot]
3cf1ce2b5d chore(deps): update dependency @reduxjs/toolkit to v2 (#2369)
* fix(deps): update dependency @reduxjs/toolkit to v2

* chore: minor updates to work with RTK version 2

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Braden MacDonald <braden@opencraft.com>
2025-12-10 22:46:58 +00:00
dependabot[bot]
7eea38e623 chore(deps): bump express from 4.21.2 to 4.22.1 (#2726)
Bumps [express](https://github.com/expressjs/express) from 4.21.2 to 4.22.1.
- [Release notes](https://github.com/expressjs/express/releases)
- [Changelog](https://github.com/expressjs/express/blob/v4.22.1/History.md)
- [Commits](https://github.com/expressjs/express/compare/4.21.2...v4.22.1)

---
updated-dependencies:
- dependency-name: express
  dependency-version: 4.22.1
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-10 14:38:49 -08:00
renovate[bot]
128de3ffc4 chore(deps): update react-router monorepo to v6.30.2 (#2742)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-10 14:03:43 -08:00
Anton Melser
6724e1a296 docs: Generify currently supported node version (#2744)
- remove some non-meaningful trailing whitespace
2025-12-10 19:37:30 +00:00
Areeb Sajjad
8fc00709b4 chore: upgrade react-datepicker (#2709) 2025-12-10 11:29:19 -08:00
renovate[bot]
f367a7641d fix(deps): update dependency @tanstack/react-query to v5.90.12 (#2741)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-08 14:41:03 -05:00
edX requirements bot
49d809b809 chore: update browserslist DB (#2740)
Co-authored-by: abdullahwaheed <42172960+abdullahwaheed@users.noreply.github.com>
2025-12-08 00:24:10 +00:00
Chris Chávez
dad736f9d1 refactor: Migration of course details to React query (#2724)
- Migrates the `courseDetails` part from the Redux Store to React Query.
- Creates a new `CourseAuthoringContext` 
- Update the pages in `<CourseAuthoringRoutes>` to use the newly created context.
- Migrates some files to Typescript
- Migrates some tests to use `src/testUtils.tsx`
2025-12-06 00:14:32 +00:00
Chris Chávez
50f4f70671 feat: Import Course Details Page [FC-0112] (#2664)
Implements all the states for the Import Course Details
2025-12-05 12:03:55 -05:00
Rodrigo Mendez
0796e897ae feat: Implement querying openedx-authz for publish permissions (#2685) 2025-12-04 14:40:32 -05:00
Chris Chávez
b76b003e01 refactor: Migrate Help Urls from Redux store to React Query (#2714) 2025-12-02 22:00:06 +00:00
renovate[bot]
e95758cc55 chore(deps): update dependency @types/lodash to v4.17.21 (#2716)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-01 18:07:35 -05:00
renovate[bot]
c7ab2b2837 chore(deps): update dependency @types/react to v18.3.27 (#2717)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-01 18:07:02 -05:00
edX requirements bot
dfe87f983b chore: update browserslist DB (#2715)
Co-authored-by: abdullahwaheed <42172960+abdullahwaheed@users.noreply.github.com>
2025-12-01 00:25:55 +00:00
Muhammad Anas
f9e09f0601 fix: replace hardcoded edX with platformName in basicBannerTitle message (#2711) 2025-11-28 13:26:00 -05:00
Navin Karkera
0c1554b337 feat: placeholder blocks for failed import blocks (#2703)
Adds placeholder blocks in home page and respective collections tab in library for failed blocks during import from course.
2025-11-27 12:42:21 -05:00
Daniel Wong
294fe42942 feat: add support for origin server and user info (#2663)
* feat: add support for origin server and user info

* test: add coverage for restore archive summary

* test: increase coverage for restore archive summary

* fix: address comments
2025-11-27 10:04:15 -05:00
Chris Chávez
95ec41d2e7 feat: dock buttons to the bottom in import course stepper (#2694) 2025-11-26 18:18:41 -05:00
renovate[bot]
e620d70793 chore(deps): update dependency @openedx/paragon to v23.18.1 (#2701)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-26 18:15:14 -05:00
Rahat Ali
fa562a691b refactor: Replace ProctorTrack references with Generic Checks (#2645)
This pull request removes frontend-level handling of the ProctorTrack
proctoring provider from the frontend-app-authoring application.

As part of the broader ProctorTrack deprecation effort
(openedx/edx-platform#36329), the backend now provides generic proctoring
configuration rules instead of vendor-specific conditions. The frontend has
been updated to rely on these generic rules while still performing client-side
validation.

By removing the ProctorTrack-specific logic and using the backend’s generic
configuration model, this change keeps frontend-app-authoring aligned with
current backend behavior and avoids relying on deprecated vendor-specific
handling.

Co-authored-by: Taimoor  Ahmed <taimoor.ahmed@A006-01711.local>
2025-11-26 12:26:30 -05:00
Kyle McCormick
70c19a3ffb fix: "Back up" is two words when used as a verb (#2696)
There is a new menu item "Backup to local archive". Backup is the correct
spelling when using it as a noun or adjective, but the menu item uses as a
verb, so it should be two words, back up, i.e. "Back up to local archive"
2025-11-26 17:15:51 +00:00
Navin Karkera
ef36156d55 fix: consider children blocks of unsupported blocks while analyzing course import (#2684)
The analysis step before importing a course considers the parent block while counting unsupported blocks but does not include children in the unsupported count.

We fetch usage_keys of all unsupported blocks and fetch the children blocks that contain these usage_keys in their breadcrumb field i.e., they are part of the unsupported blocks.
2025-11-25 16:24:01 -05:00
dependabot[bot]
735159311d chore(deps): bump actions/checkout from 5 to 6 (#2692)
Bumps [actions/checkout](https://github.com/actions/checkout) from 5 to 6.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v5...v6)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-11-24 16:02:45 -05:00
edX requirements bot
4fd52b9a4d chore: update browserslist DB (#2691)
Co-authored-by: abdullahwaheed <42172960+abdullahwaheed@users.noreply.github.com>
2025-11-24 00:23:39 +00:00
Braden MacDonald
4a81ddbe74 refactor: convert <UnitButton> and <UnitIcon> to TSX (#2649) 2025-11-20 14:29:00 -05:00
Navin Karkera
2215fc53cc fix: don't revert to advanced editor if block contains copied_from fields (#2661) 2025-11-19 15:43:52 -05:00
renovate[bot]
23ab74da14 chore(deps): update dependency formik to v2.4.9 (#2655)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-18 18:19:25 -05:00
dependabot[bot]
1d385c6b4d chore(deps): bump js-yaml (#2659)
Bumps [js-yaml](https://github.com/nodeca/js-yaml).

Updates `js-yaml` from 3.14.1 to 3.14.2
- [Changelog](https://github.com/nodeca/js-yaml/blob/master/CHANGELOG.md)
- [Commits](https://github.com/nodeca/js-yaml/compare/3.14.1...3.14.2)

Updates `js-yaml` from 4.1.0 to 4.1.1
- [Changelog](https://github.com/nodeca/js-yaml/blob/master/CHANGELOG.md)
- [Commits](https://github.com/nodeca/js-yaml/compare/3.14.1...3.14.2)

---
updated-dependencies:
- dependency-name: js-yaml
  dependency-version: 3.14.2
  dependency-type: indirect
- dependency-name: js-yaml
  dependency-version: 4.1.1
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-11-18 13:45:45 -05:00
renovate[bot]
acbd1de548 chore(deps): update dependency react-datepicker to v8 (#2572)
* fix(deps): update dependency react-datepicker to v8

* chore(deps): update code to work with react-datepicker v8

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Braden MacDonald <braden@opencraft.com>
2025-11-18 13:10:38 -05:00
Rômulo Penido
650bb62fa7 fix: migrate library alert text (#2651)
This PR updates the alert texts on the Migrate Libraries Stepper
2025-11-18 12:09:03 -05:00
Navin Karkera
e2f1aedf9a feat: import analysis step (#2657)
Shows course analysis information in review import details step in course import stepper page. Also handles alerts based on the import status, like, reimport or unsupported number of blocks.
2025-11-18 11:41:27 -05:00
Kyle McCormick
5fadccabe2 fix: Rename builtin discussion providers, "edX" -> "Open edX" (#2660) 2025-11-18 14:55:33 +00:00
renovate[bot]
daed664404 chore(deps): update dependency fast-xml-parser to v5.3.2 (#2594)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-17 18:07:45 +00:00
Asad Ali
495a682926 fix: do not reload multiple tabs on block save (#2600) 2025-11-17 12:54:07 -05:00
renovate[bot]
6f41cd76da chore(deps): update codemirror (#2535)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-17 17:07:01 +00:00
renovate[bot]
64489e1b4d chore(deps): update dependency @openedx/paragon to v23.18.0 (#2643)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-17 17:06:10 +00:00
renovate[bot]
91c813b214 fix(deps): update dependency @tanstack/react-query to v5.90.10 (#2654)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-17 17:05:46 +00:00
Chris Chávez
2e7a29465f refactor: Update title in ImportStepperModal (#2658)
Updates the title of one card from "Import Details" to "Analysis Details"
2025-11-17 16:51:44 +00:00
edX requirements bot
0f58329cb4 chore: update browserslist DB (#2653)
Co-authored-by: abdullahwaheed <42172960+abdullahwaheed@users.noreply.github.com>
2025-11-17 00:22:55 +00:00
Chris Chávez
54cfbeb756 feat: import course in library stepper [FC-0112] (#2567)
- Implemented the course import stepper described in https://github.com/openedx/frontend-app-authoring/issues/2524
- Adds the new `ENABLE_COURSE_IMPORT_IN_LIBRARY` flag
2025-11-14 13:07:00 -05:00
Muhammad Anas
7cf01de84c fix: grading settings save button stuck in pending state (#2614) 2025-11-14 08:54:57 -05:00
Navin Karkera
a1abd43a11 refactor: rename team access navbar and sidebar (#2644) 2025-11-13 20:27:06 -05:00
Muhammad Anas
8f06263e27 fix: unit button active state (#2617) 2025-11-13 12:08:18 -05:00
Braden MacDonald
e10ab270dd chore: don't name unused errors in catch expressions (#2591) 2025-11-12 18:11:22 -05:00
Braden MacDonald
a5d65abea2 chore: fix no-base-to-string (#2597) 2025-11-12 13:34:00 -05:00
Braden MacDonald
5ec00236cb chore: fix unnecessary 'undefined' types (#2589) 2025-11-12 13:26:01 -05:00
Devasia Joseph
2530b01b82 fix: restrict single select questions to one correct answer (#2618) 2025-11-12 17:43:42 +05:00
renovate[bot]
13c51ce5a8 chore(deps): update dependency @tanstack/react-query to v5.90.7 (#2611)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-10 10:49:02 -05:00
edX requirements bot
b2cfafc00e chore: update browserslist DB (#2610)
Co-authored-by: abdullahwaheed <42172960+abdullahwaheed@users.noreply.github.com>
2025-11-10 00:22:49 +00:00
Rômulo Penido
6d619b9c40 feat: add course import page [FC-0112] (#2580)
Adds the Library Import Home, which lists course migrations to the library
2025-11-07 16:32:11 -05:00
Muhammad Arslan
6afe6095a5 fix: self-closing script tag fixed for TinyMceEditor (#2510)
(not really valid HTML but preserves backwards compatibility)
2025-11-06 09:38:04 -08:00
Muhammad Arslan
1b357cb2b6 fix: broken Course Overview editor on Schedule & Details page (#2599) 2025-11-05 15:15:31 -08:00
Chris Chávez
2de987b254 style: Update some texts in legacy libraries migration flow (#2601) 2025-11-05 18:13:32 -05:00
renovate[bot]
4299bf16b4 chore(deps): update dependency @openedx/paragon to v23.16.0 (#2583)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-05 15:07:54 -08:00
Navin Karkera
5cda284cdb fix: pass readOnly flag to Header in library authoring page (#2598) 2025-11-04 17:56:37 -05:00
Navin Karkera
436ac3155d feat: nav dropdowns in library authoring view (#2556)
Updates navbar in library authoring page to include `Team Access` and `Import` menu options. Clicking on `Team Access` button opens Team management modal.

As per this new PR: https://github.com/openedx/frontend-app-authoring/pull/2570, if admin console url is set, it should be used instead of team access modal. So updated this PR accordingly.
2025-11-03 17:06:50 -05:00
renovate[bot]
86a7e06a3c chore(deps): update dependency @tanstack/react-query to v5.90.6 (#2595)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-03 09:31:38 -08:00
Navin Karkera
bd82c1d33d fix: publish status of container on adding new children (#2587)
Updates publish status of container when adding new child components to a unit or other containers.
2025-11-03 10:01:30 -05:00
Navin Karkera
75ae9d549c feat: handle duplicate children in container pages [FC-0112] (#2584)
If we have duplicate container or component in parent page in library, clicking on one of them selects both and removing any one from the parent blocks removes all instances.
This PR handles duplicates by including index/order_number of each child component in the url and using it to exclude a single instance and update parent structure.
2025-11-03 09:59:37 -05:00
edX requirements bot
cec074e6d4 chore: update browserslist DB (#2593)
Co-authored-by: abdullahwaheed <42172960+abdullahwaheed@users.noreply.github.com>
2025-11-03 00:23:01 +00:00
Braden MacDonald
36c9eba66d refactor: remove remaining injectIntl(), ban it using eslint (#2585)
This finished the removal of `injectIntl` from this codebase, and configures a new eslint rule to ban it completely.
2025-10-30 20:10:52 -05:00
Navin Karkera
2dc087f87a feat: differentiate between renamed and data edit in sync preview diff [FC-0112] (#2577)
Use `downstream_customized` field from upstream link to determine whether the text component was locally renamed or content updated or both and display correct notes in preview diff.
Also update libraries v2 alert wording as per https://github.com/openedx/frontend-app-authoring/issues/2169#issuecomment-3434735901
2025-10-29 17:24:04 -05:00
Javier Ontiveros
9b77a40284 feat: add restore from file UI for libraries v2 (#2558) 2025-10-29 21:37:35 +00:00
renovate[bot]
871d98828c chore(deps): update dependency @openedx/paragon to v23.15.1 (#2569)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-29 09:31:41 -07:00
renovate[bot]
f116740184 chore(deps): update github artifact actions (#2571)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-27 10:49:02 -07:00
renovate[bot]
cd967a9878 chore(deps): update dependency @edx/frontend-platform to v8.5.2 (#2568)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-27 10:47:21 -07:00
Farhaan Bukhsh
d8805bf2b4 chore: Updates frontend-component-header to include new slots (#2576)
Signed-off-by: Farhaan Bukhsh <farhaan@opencraft.com>
2025-10-27 16:07:51 +00:00
Diana Olarte
0972b7e62d feat: [FC-0099] redirect to admin console MFE (#2570)
* feat: redirect to admin console MFE

This PR redirects to admin console MFE if the URL is configured, to leverage the new experience of team management this is part of the AuthZ project https://github.com/openedx/openedx-authz/tree/main/docs/decisions

* refactor: split the logic into 2 variables for readability
2025-10-27 15:59:25 +00:00
Kshitij Sobti
15a728d0e7 feat: Add slots for video and file upload components and alerts (#1523)
This change add plugin slots for the file and video upload components, and the alerts components on those pages.
2025-10-24 14:39:50 -07:00
Braden MacDonald
157e2464aa test: fix test code improperly using/missing 'await' (#2560) 2025-10-24 21:31:51 +00:00
Braden MacDonald
c54c21e2b4 test: fix warnings when StrictDict values are checked with propTypes (#2565) 2025-10-24 14:16:07 -07:00
Muhammad Anas
dc05ccfd16 feat: add never_but_include_grade visibility option (#2489)
This PR introduces a new visibility option for assignment scores:

“Never show individual assessment results, but show overall assessment results after the due date.”

With this option, learners cannot see question-level correctness or scores at any time. However, once the due date has passed, they can view their overall score in the total grades section on the Progress page.
2025-10-24 15:08:32 -03:00
Chris Chávez
106f22b3c2 chore: Enable legacy libraries migration by default (#2561) 2025-10-22 18:20:55 -05:00
Chris Chávez
76d8b2e03a feat: Add error messages for partial migration [FC-0107] (#2555)
Adds the error messages for partial migrations
2025-10-22 17:07:24 -05:00
Kyle McCormick
5ce61fa5e5 feat: Add ability to create Legacy Libraries (#2551)
This adds a CreateLegacyLibrary component. It functions the same as
CreateLibrary, but it calls the V1 (legacy) creation REST API rather the V2
(new/beta) REST API.

This reinstates, in the MFE, something that was possible using the legacy
frontend until it was prematurely removed by
https://github.com/openedx/edx-platform/pull/37454. 

We plan to re-remove this ability between Ulmo and Verawood as part of:
https://github.com/openedx/edx-platform/issues/32457.
So, we have intentionally avoided factoring out common logic between
CreateLibrary and CreateLegacyLibrary, ensuring that the latter
remains easy to remove and clean up.
2025-10-22 21:33:59 +00:00
Chris Chávez
46fa17ea83 fix: UI fixes in legacy library migrator
- Keep state of all migration steps on nevigation
- Reword alert in confirm dialog
- Add scroll to help sidebar in migration
- Keep the same migration filter
2025-10-22 21:24:52 +00:00
Chris Chávez
9f1604110b feat: Deleted/Added level diff in sync modal [FC-0097] (#2549)
- Adds the Deleted and the Added level diff in sync container modal.
- Fix the icons in the sync container modal
2025-10-22 20:53:12 +00:00
Muhammad Anas
82a3c2815b feat: enable markdown to OLX conversion (#2518) 2025-10-21 16:58:20 -07:00
renovate[bot]
191be55b2e chore(deps): update dependency @tanstack/react-query to v5.90.5 (#2545)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-20 17:55:40 -07:00
renovate[bot]
8f7e48421f chore(deps): update actions/setup-node action to v6 (#2546)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-20 17:32:11 -07:00
dependabot[bot]
b6de9a8883 chore(deps): bump actions/setup-node from 5 to 6 (#2547)
Bumps [actions/setup-node](https://github.com/actions/setup-node) from 5 to 6.
- [Release notes](https://github.com/actions/setup-node/releases)
- [Commits](https://github.com/actions/setup-node/compare/v5...v6)

---
updated-dependencies:
- dependency-name: actions/setup-node
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-20 17:29:38 -07:00
Javier Ontiveros
7bfc73073b [feature] add backup view for libraries v2 (#2532)
* feat: add backup view for libraries v2

* chore: updated paths and cleanup

* chore: cleanup text

* chore: added test

* chore: fix contracts after rebase

* chore: more tests to improve coverage

* chore: more test for coverage

* chore: more test for coverage

* chore: fixed lint issues

* chore: update naming for a more semantic one

* chore: changed fireEvent to userEvent

* chore: improved queryKeys

* chore: lint cleanup

* chore: changed tests and time to 1min

* chore: even more tests

* chore: split hook for library menu items

* chore: fixed typo on refactor

* chore: improved test to use available mocks

* chore: change from jest.mocks to spyon

* chore: update test based on commets

* chore: update test to get URL from a better place

* chore: added extra getters for new endpoints

* chore: update test to prevent issues with useContentLibrary

* chore: added comments for clarity

* chore: lint fix

* chore: updated url handle to use full URL

* chore: linting fixes
2025-10-20 16:51:05 -06:00
renovate[bot]
98009b3e6a chore(deps): update dependency @edx/frontend-component-footer to v14.9.3 2025-10-20 16:34:05 -03:00
Navin Karkera
e80930e06f fix: add components button in item bank children page (#2491)
`Add components` button in item bank children page was not working.
2025-10-20 12:09:58 -05:00
edX requirements bot
66dad5ff32 chore: update browserslist DB (#2544)
Co-authored-by: abdullahwaheed <42172960+abdullahwaheed@users.noreply.github.com>
2025-10-20 00:22:22 +00:00
bydawen
e4ea69266f fix: pointer-events issue on toast container (#2139) 2025-10-17 14:26:03 -07:00
Rômulo Penido
8b6a350808 feat: add copy container menu (#2538)
Adds copy support for Library Containers and updates the useClipboard hook to provide info on copied containers.
2025-10-17 14:11:18 -05:00
renovate[bot]
a56faf8ca7 fix(deps): update dependency @tinymce/tinymce-react to v6 (#2536)
* fix(deps): update dependency @tinymce/tinymce-react to v6

* refactor: validating typing of tinymce-react usage (no runtime changes)

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Braden MacDonald <braden@opencraft.com>
2025-10-16 22:07:27 +00:00
renovate[bot]
77215eeb5e chore(deps): update dependency @openedx/paragon to v23.14.9 (#2533)
* chore(deps): update dependency @openedx/paragon to v23.14.9

* test: update tests because of paragon role=presentation modal changes

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Braden MacDonald <braden@opencraft.com>
2025-10-16 16:53:39 +00:00
Chris Chávez
311bef67ed feat: Shows an alert in container sync if the only change is a local override to a text component [FC-0097] (#2516)
- Implements the alert described in https://github.com/openedx/frontend-app-authoring/issues/2438#issuecomment-3358670967
2025-10-15 19:17:56 -05:00
Chris Chávez
195249ef26 feat: Legacy libraries migration help sidebar [FC-0097] (#2503)
- Implements the Legacy Libraries Migration Help Sidebar
- Shows the sidebar in the studio home
- Shows the sidebar in the Legacy Libraries Migration Page
2025-10-15 20:35:08 +00:00
Ihor Romaniuk
4a26a86c90 fix: answer range format validation in numerical input problems (#2426) 2025-10-15 11:33:31 -07:00
vladislavkeblysh
411d4f053c fix: fixed delete for additional video url fields in video editor (#2470) 2025-10-15 09:47:03 -07:00
Chris Chávez
9c0b545b2f feat: Connect bulk migration backend with frontend (#2493)
- Connects the `Confirm` button with the bulk migrate backend
- Updates the library page to get the migration task status and refresh the component on success.
2025-10-14 00:04:36 +00:00
Ahtesham Quraish
cd36407457 fix: [Library browse] Unintended scroll upon unit selection #2452 (#2472) 2025-10-13 09:22:22 -05:00
Chris Chávez
46d2465064 fix: Multiple UI/UX improvements (#2529)
This includes multiple improvements described in https://github.com/openedx/frontend-app-authoring/issues/2528
2025-10-13 09:21:00 -05:00
edX requirements bot
6c829b9421 chore: update browserslist DB (#2534)
Co-authored-by: abdullahwaheed <42172960+abdullahwaheed@users.noreply.github.com>
2025-10-13 00:22:24 +00:00
Kyle McCormick
1d6fdc39fd feat: frontend-component-header to 8.0.0, to rm Maintenance link (#2521)
BREAKING CHANGE: Removes Maintenance link in header, part of
https://github.com/openedx/edx-platform/issues/36263

Full diff:
https://github.com/openedx/frontend-component-header/compare/v6.2.0...v8.0.0
2025-10-10 12:38:02 -04:00
Ahtesham Quraish
04e9a253ba fix: library card icons are not vertically aligned #2473 (#2474) 2025-10-10 09:01:13 -07:00
Ahtesham Quraish
8470d7cd4d fix: Rename field should use full available width #2337 (#2436) 2025-10-10 09:00:40 -07:00
renovate[bot]
b774084a10 chore(deps): update dependency @openedx/paragon to v23.14.5 (#2530)
* chore(deps): update dependency @openedx/paragon to v23.14.5

* fix: correct type of ContainerPropsType

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Braden MacDonald <braden@opencraft.com>
2025-10-09 17:14:33 +00:00
MuPp3t33r
aadccc748c fix: course assets state persistence bug (#2401) 2025-10-08 11:36:26 -07:00
Navin Karkera
c4a439df47 fix: show before and after title in diff preview (#2509)
Fix display of Before and After display name in section/subsection sync preview modal.
2025-10-07 14:52:22 -05:00
renovate[bot]
8fe5fb6a20 chore(deps): update dependency universal-cookie to v8 (#2512)
* fix(deps): update dependency universal-cookie to v8

* refactor: validate typing of universal-cookie usages

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Braden MacDonald <braden@opencraft.com>
2025-10-06 23:08:46 +00:00
renovate[bot]
0315c05e11 chore(deps): update dependency fast-xml-parser to v5 (#2515)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-06 22:41:17 +00:00
renovate[bot]
a5d51ce4f4 chore(deps): update dependency @types/react to v18.3.26 (#2513)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-06 15:25:18 -07:00
renovate[bot]
3a6378e569 chore(deps): update dependency @testing-library/jest-dom to v6.9.1 (#2508)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-06 14:40:55 -07:00
renovate[bot]
e37f2e0071 chore(deps): update dependency @types/react to v18.3.25 (#2507)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-06 14:40:18 -07:00
edX requirements bot
835de77385 chore: update browserslist DB (#2506)
Co-authored-by: abdullahwaheed <42172960+abdullahwaheed@users.noreply.github.com>
2025-10-06 00:21:27 +00:00
renovate[bot]
7f23e9b585 chore(deps): update dependency @openedx/paragon to v23.14.4 (#2504)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-04 00:30:06 +00:00
renovate[bot]
32ed2f183b chore(deps): update codemirror (#2487)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-04 00:29:54 +00:00
renovate[bot]
292068af6e chore(deps): update dependency @tanstack/react-query to v5.90.2 (#2488)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-03 17:17:05 -07:00
Chris Chávez
a975f3b716 feat: New modal to sync changes for standalone text components [FC-0097] (#2449)
Adds a new sync modal when a Text component has local changes.
2025-09-30 09:45:13 -05:00
Chris Chávez
1c7ad2f725 feat: Migrate Legacy Libraries Flow [FC-0097] (#2425)
- Creates all steps of the flow described in https://github.com/openedx/frontend-app-authoring/issues/2201
2025-09-29 18:02:15 -05:00
renovate[bot]
7ba3db0187 chore(deps): update dependency @openedx/paragon to v23.14.3 (#2485)
* chore(deps): update dependency @openedx/paragon to v23.14.3

* fix: ref was invalid and causing console error

"Function components cannot be given refs. Attempts to access this ref will fail. Did you mean to use React.forwardRef()?"

(Paragon does not support refs for IconButtonWithTooltip, although it could be added at any time.)

* fix: react query console error in tags drawer

"No queryFn was passed as an option, and no default queryFn was found. The queryFn parameter is only optional when using a default queryFn."

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Braden MacDonald <braden@opencraft.com>
2025-09-29 21:07:49 +00:00
edX requirements bot
fdf98a1400 chore: update browserslist DB (#2486)
Co-authored-by: abdullahwaheed <42172960+abdullahwaheed@users.noreply.github.com>
2025-09-29 00:21:05 +00:00
Chris Chávez
56d3eede64 feat: Adding loading and done states to the publish library button [FC-0097] (#2237)
Adds loading and done states to the publish library button.
2025-09-26 15:15:23 +00:00
oleksandr.buhaienko
c5de944d72 test: Remove support for Node 20 2025-09-26 10:29:24 -03:00
Rômulo Penido
523dd1f389 feat: add library v2 alert (#2413)
Adds an Alert to the Legacy Library Page to notify the user of the process of deprecating Legacy Libraries and a Button to open the Migrate Library interface.
2025-09-25 16:01:57 -05:00
Navin Karkera
cffc4d77c9 feat: migration filter and search bar in legacy libraries tab [FC-0097] (#2421)
Adds search bar and migration filter in legacy libraries tab
2025-09-25 17:00:26 +00:00
Navin Karkera
25160347b3 feat: container library-course sync diff prevew [FC-0097] (#2464)
Container sync preview implemented
2025-09-25 11:13:58 -05:00
Navin Karkera
d63680083d feat: show migration status in libraries list [FC-0097] (#2417)
Adds migration status to library cards in legacy libraries tab in studio home.
Also converts javascript files to typescript and replaces redux with react query for related api calls.
2025-09-25 10:49:13 -05:00
bydawen
39e5f89b45 build: Upgrade to Node 24 (#2459) 2025-09-22 09:49:04 -07:00
edX requirements bot
7e81b52583 chore: update browserslist DB (#2469)
Co-authored-by: abdullahwaheed <42172960+abdullahwaheed@users.noreply.github.com>
2025-09-22 00:22:14 +00:00
Rômulo Penido
1efa94d410 fix: update delete and remove modals design [FC-0097] (#2453)
Changes the Remove/Delete Component/Container dialogs according to the design. It also standardized the messages from Components and Containers.
2025-09-18 16:27:35 -05:00
bydawen
d98a34ac3f test: Add Node 24 to CI matrix (#2443) 2025-09-18 10:09:51 -07:00
Akanshu Aich
8b530481de fix: remove unused ENABLE_HOME_PAGE_COURSE_API_V2 config reference (#2461) 2025-09-18 09:49:03 -07:00
Muhammad Faraz Maqsood
9f6a882e61 fix: course optimizer issues (#2450)
- don't show `Scan results` heading until there are some results to show.
- change spinner from paragon with spinner icon which looks better than spinner itself.
- disable `update all` button when single update prev Link is in progress.

Co-authored-by: Muhammad Faraz  Maqsood <faraz.maqsood@A006-01130.local>
2025-09-18 10:44:18 +05:00
renovate[bot]
b95b3a60ad chore(deps): update dependency @tanstack/react-query to v5 (#2404)
* fix(deps): update dependency @tanstack/react-query to v5

* chore: update for compatibility with React Query v5

* chore: update for compatibility with React Query v5

* test: update tests

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Braden MacDonald <braden@opencraft.com>
2025-09-17 16:10:08 -07:00
Navin Karkera
61c87fe6a6 feat: allow editing imported unit blocks (#2405)
Allows authors to edit imported unit display name in outline.
2025-09-17 12:27:04 -05:00
renovate[bot]
c21b664a8b chore(deps): update codemirror to v6.4.10 (#2432)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-16 10:50:08 -07:00
Chris Chávez
71376fa22b feat: ENABLE_LEGACY_LIBRARY_MIGRATOR flag added (#2440)
- Adds the `ENABLE_LEGACY_LIBRARY_MIGRATOR` flag. **Reason:** The migrator frontend is finishing before the backend; this flag is mainly to hide it until it is fully connected and working with the backend.
- Puts the migration warning under the new flag.
2025-09-12 11:20:31 -05:00
Chris Chávez
23b4f4731e fix: Use the correct branch of openedx/.github repo for add-cc-to-board (#2441) 2025-09-11 12:33:10 -07:00
Chris Chávez
ab645ad86b feat: add-to-cc-board Github action created. (#2302)
- This is a label trigger to add an Issue or PR to the front-end [Core Contributor project board](https://github.com/orgs/openedx/projects/80).
- This action uses the `Core Contributor assignee` label as a trigger.
- This action reuses https://github.com/openedx/.github/pull/169
- When you add the label to an issue or PR, it is automatically added to the board https://github.com/orgs/openedx/projects/80
2025-09-10 17:10:38 -05:00
Navin Karkera
720b591add feat: display only one card action overflow menu (#2427)
Instead of stopping whole click event propagation from actions to card element, specifically stop click event if the source target is actions menu.
2025-09-08 16:47:17 -05:00
renovate[bot]
87239ab723 chore(deps): update dependency @types/react to v18.3.24 (#2431)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-08 09:41:52 -07:00
Asad Ali
0117c1eae3 fix: allow thumbnail upload on Videos page if no thumbnail (#2388)
* fix: allow thumbnail upload if no thumbnail

* fix: improve thumbnail upload impl

* test: fix tests

* test: fix tests

* fix: do not show thumbnail upload if not allowed

* test: fix coverage

* test: add thumbnail test

* fix: display thumbnail overlay when video status is success
2025-09-08 09:40:32 -07:00
Ahtesham Quraish
0f7c8de882 fix: files page web url missing #2409 (#2420)
Co-authored-by: Ahtesham Quraish <ahtesham.quraish@A006-01455.local>
2025-09-08 09:33:19 -07:00
dependabot[bot]
387c45a5b2 chore(deps): bump actions/setup-node from 4 to 5 (#2433)
Bumps [actions/setup-node](https://github.com/actions/setup-node) from 4 to 5.
- [Release notes](https://github.com/actions/setup-node/releases)
- [Commits](https://github.com/actions/setup-node/compare/v4...v5)

---
updated-dependencies:
- dependency-name: actions/setup-node
  dependency-version: '5'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-09-08 09:00:29 -07:00
edX requirements bot
6377fbd896 chore: update browserslist DB (#2430)
Co-authored-by: abdullahwaheed <42172960+abdullahwaheed@users.noreply.github.com>
2025-09-08 00:22:14 +00:00
Chris Chávez
67fab054ab feat: Secondary publish workflow for components [FC-0097] (#2399)
- Adds the new publish button and the new confirm publish box for components.
- Deletes the old confirm publish modal for components
- Adds the publish button next to the open button for containers 
- Update changes to grand-parent and grand-child items.
2025-09-05 17:12:40 +00:00
renovate[bot]
a7860b8392 chore(deps): update dependency @edx/frontend-component-header to v6.6.1 (#2407)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-04 02:20:41 +00:00
renovate[bot]
3082eca91c chore(deps): update dependency @edx/frontend-component-footer to v14.9.1 (#2415)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-03 19:07:46 -07:00
Rômulo Penido
2fb04d670f feat: add legacy library alert (#2412)
Adds an Alert to the Legacy Library Page to notify the user of the process of deprecating Legacy Libraries and a Button to open the Migrate Library interface.
2025-09-02 14:23:49 -05:00
Samuel Allan
f79b65c273 fix: update frontend-build to fix install issues (#2387)
Earlier versions of @openedx/frontend-build used on older version of
'sharp', which caused intermittent installation issues. The version of
'sharp' was updated in @openedx/frontend-build to fix these issues, so
the frontend-build version can be updated here, to fix the issues in
this project too. See
https://github.com/openedx/frontend-build/issues/664 and
https://github.com/openedx/frontend-build/pull/665 for more information.

The frontend-build dependency was updated by:

```
npm install --package-lock-only @openedx/frontend-build
```

Private-ref: https://tasks.opencraft.com/browse/BB-9953
2025-09-02 09:48:30 -07:00
Pandi Ganesh
472d77823f feat: Enhance Course Optimizer Page with Previous Run Links and Improved UI (#2356)
* feat: enhance course optimizer page design in studio

* feat: enhance course optimizer with prev run links update

* fix: increase container size and resolve style issues

* fix: enhance code structure and i18n support
2025-09-02 17:01:28 +05:00
edX requirements bot
09f4304daa chore: update browserslist DB (#2414)
Co-authored-by: abdullahwaheed <42172960+abdullahwaheed@users.noreply.github.com>
2025-09-01 00:23:43 +00:00
Rômulo Penido
950bfee7c1 feat: add unlink upstream menu [FC-0097] (#2393)
Adds the Unlink feature to the Course Outline for Sections, Subsections and Units.
2025-08-28 11:44:15 -05:00
Pradeep
0f2dd4a88f fix: ensure hyperlink renders correctly based on videoSource presence (#2400)
* fix: ensure hyperlink renders correctly based on videoSource presence

* refactor: remove unnecessary blank lines in VideoPreviewWidget tests
2025-08-28 14:44:17 +05:00
Chris Chávez
6646c8ed0f style: Fixing nits about sync units [FC-0097] (#2319)
* Stay visible the sync icon in the course outline
* Update the message in the sync unit/subsection/section modal
* Add a tooltip to the edit and sync button.
2025-08-27 19:29:24 -05:00
Vivek Ambaliya
9a9806ccad feat: add new help section in course team page 2025-08-27 14:08:16 +05:00
Muhammad Faraz Maqsood
86e9c6b1fa fix: tinyMCE images previews in image selection modal 2025-08-26 11:28:27 +05:00
renovate[bot]
2a787953ef chore(deps): update dependency @testing-library/jest-dom to v6.8.0 (#2403)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-25 12:29:24 -07:00
Braden MacDonald
c90195e0fd refactor: move 'isMarkdownEditorEnabledForContext' into EditorContext (#2398)
This just moves one single state variable, `isMarkdownEditorEnabledForCourse` out of the Redux state and into the `EditorContext`.
2025-08-25 12:31:39 -05:00
edX requirements bot
dfd3b93f0a chore: update browserslist DB (#2402)
Co-authored-by: abdullahwaheed <42172960+abdullahwaheed@users.noreply.github.com>
2025-08-25 00:22:18 +00:00
renovate[bot]
ae449b914b chore(deps): update dependency @openedx/paragon to v23.14.2 (#2375)
* fix(deps): update dependency @openedx/paragon to v23.14.2

* chore: fix invalid 'size' passed to <Spinner>

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Braden MacDonald <braden@opencraft.com>
2025-08-22 16:45:18 +00:00
Kshitij Sobti
5c006af6ec fix: restore styling for header title and button on outline page (#2385)
In a previous PR #2374 some of the styling applied in addition to truncation
was lost. This restores that.
2025-08-22 09:14:48 -07:00
Braden MacDonald
641fc589a4 Add TypeScript types to the redux state (#2394)
Adds some TypeScript types to the global redux state that's in `src/store.ts`. I've only added types for a few parts of the state but already it's caught quite a few bugs in the code, which I've tried to fix in this PR.
2025-08-22 11:00:19 -05:00
Chris Chávez
0c88fd6da9 feat: show sync button on section/subsections [FC-0097] (#2324)
- Adds the sync button on section/subsection cards
2025-08-21 21:38:16 +00:00
Braden MacDonald
8e680dc8d4 refactor: minor typing improvements (#2395)
This makes some minor typing improvements in our test code. Specifically instead of just `{...} as XBlock` which is an unsafe cast, we can use `{...} satisfies Partial<XBlock> as XBlock` which is a safer cast that lets you omit fields but requires that the fields you do include have the correct type.
2025-08-21 12:24:18 -05:00
Jillian
2f6e510b09 Display Container Publish status and confirm before publish (#2186)
Updates the Container sidebar to display:

* A confirmation step before publishing the container.
* Text + a full hierarchy to better demonstrate what will be published when the container is published.
2025-08-20 13:22:30 -05:00
Ahtesham Quraish
87af7e8973 refactor: Replace of injectIntl with useIntl() part 8 #2288 (#2357)
Co-authored-by: Ahtesham Quraish <ahtesham.quraish@192.168.1.4>
2025-08-20 11:07:24 -07:00
Ahtesham Quraish
33c445ebc2 refactor: Replace of injectIntl with useIntl() part 6 - files-and-videos/generic/EditFileErrors (#2358)
Co-authored-by: Ahtesham Quraish <ahtesham.quraish@192.168.1.4>
2025-08-20 10:58:02 -07:00
dependabot[bot]
7825bcde75 chore(deps): bump brace-expansion (#2381)
Bumps  and [brace-expansion](https://github.com/juliangruber/brace-expansion). These dependencies needed to be updated together.

Updates `brace-expansion` from 2.0.1 to 2.0.2
- [Release notes](https://github.com/juliangruber/brace-expansion/releases)
- [Commits](https://github.com/juliangruber/brace-expansion/compare/v2.0.1...v2.0.2)

Updates `brace-expansion` from 1.1.11 to 1.1.12
- [Release notes](https://github.com/juliangruber/brace-expansion/releases)
- [Commits](https://github.com/juliangruber/brace-expansion/compare/v2.0.1...v2.0.2)

---
updated-dependencies:
- dependency-name: brace-expansion
  dependency-version: 2.0.2
  dependency-type: indirect
- dependency-name: brace-expansion
  dependency-version: 1.1.12
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-08-18 18:19:11 -07:00
Rômulo Penido
2c9f90ba5a fix: change container sync status icon [FC-0097] (#2360)
Changes the sync icon for Sections, Subsections, and Units in case the Upstream source is deleted.
2025-08-18 17:21:47 -05:00
renovate[bot]
ada52c3169 chore(deps): update dependency @testing-library/jest-dom to v6.7.0 (#2382)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-18 09:35:24 -07:00
renovate[bot]
38b0b6543b chore(deps): update actions/checkout action to v5 (#2383)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-18 09:34:54 -07:00
Muhammad Faraz Maqsood
e6b72453b3 chore: add missing problem text for localization 2025-08-18 17:12:13 +05:00
edX requirements bot
0820a1e7df chore: update browserslist DB (#2350)
Co-authored-by: abdullahwaheed <42172960+abdullahwaheed@users.noreply.github.com>
2025-08-18 00:23:20 +00:00
Sara Burns
be82e96e6f feat: add xblockScroll event handler to iframeMessageTypes (#2363)
* fix: new message type to scroll outer window to xblock location

* fix: reset after testing

* fix: formatting

* test: add test coverage

* fix: fix test mocks

* fix: formatting

* fix: add smooth to scroll
2025-08-15 09:11:21 -04:00
renovate[bot]
b2203f0ece chore(deps): update dependency @codemirror/lang-markdown to v6.3.4 (#2367)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-15 02:31:13 +00:00
Kshitij Sobti
53e925e07a fix: Replace Truncate.Deprecated with CSS-based truncation (#2374)
The Truncate element as it exists has a bug where it can end up in an infinite loop when truncating to a very small size on mobiles. This makes the course outline view unresponsive on mobile and can lead to a crash.

This replaces the Truncate element with some CSS.
2025-08-14 19:12:07 -07:00
Chris Chávez
25830a2130 feat: Show sections/subsections/units available for sync in library sync page [FC-0097] (#2271)
- Adds Units, Subsection, and section cards in the libraries sync page.
- Rename of `mockGetEntityLinks` to `mockGetComponentEntityLinks`
- Use the top-level parent logic
- Which user roles will this change impact? "Course Author".
2025-08-14 12:59:29 -05:00
renovate[bot]
6ce7b86e83 chore(deps): update dependency @testing-library/jest-dom to v6.6.4 (#2366)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-11 14:16:14 -07:00
Victor Navarro
5bdef7cffa fix: disable special exams config if feature flag is disabled (#2325)
* fix: disable special exams config if feature flag is disabled

* test: add testcases

* fix: convert AdvancedTab to typescript
2025-08-11 13:24:38 -07:00
renovate[bot]
f0c5a513de chore(deps): update actions/download-artifact action to v5 (#2364)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-08 13:03:05 -07:00
Navin Karkera
da46fe0a48 fix: remove interactivity from section/subsection preview in sidebar (#2362)
Removes interactivity from section/subsection preview in sidebar. Also fixes an issue with unit sidebar, where users could press enter after clicking on any component and it would select it.
2025-08-08 10:45:15 -05:00
Ahtesham Quraish
7c4ef47da5 refactor: replace one injectIntl with useIntl, fix JSX return (#2354)
Co-authored-by: Ahtesham Quraish <ahtesham.quraish@192.168.1.4>
2025-08-07 13:47:43 -07:00
renovate[bot]
8003453b73 chore(deps): update dependency @edx/frontend-platform to v8.5.0 (#2355)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-07 12:52:14 -07:00
Navin Karkera
92c3a98a3d fix: show/hide add unit button based on childAddable flag of parent in unit page (#2351)
Course unit page shows Add Unit option without checking whether the parent subsection allows adding children. This fixes it.
2025-08-06 14:41:05 -05:00
dependabot[bot]
0e1550a45b chore(deps): bump js-toml from 1.0.1 to 1.0.2 (#2353)
Bumps [js-toml](https://github.com/sunnyadn/js-toml) from 1.0.1 to 1.0.2.
- [Release notes](https://github.com/sunnyadn/js-toml/releases)
- [Commits](https://github.com/sunnyadn/js-toml/compare/v1.0.1...v1.0.2)

---
updated-dependencies:
- dependency-name: js-toml
  dependency-version: 1.0.2
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-08-05 16:05:06 -07:00
renovate[bot]
15c79ceb21 chore(deps): update dependency @openedx/paragon to v23.14.1 (#2348)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-05 13:37:42 -07:00
renovate[bot]
43de4d1e32 chore(deps): update dependency @edx/frontend-component-header to v6.6.0 (#2349)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-05 13:36:10 -07:00
Braden MacDonald
591444d72d test: Clean up editor tests (#2343)
* test: improve the editorRender helper

* fix: redux state bug introduced in #2326

* test: add note for future reference about accessing the editor redux store
2025-08-05 10:41:44 -07:00
Pradeep Patro
2f9566c4f5 refactor: Problem type handling to support localization
- Updated hooks and components to utilize localized problem titles and descriptions.
- Introduced `getProblemTypes` and `getAdvanceProblems` functions for internationalization support.
- Enhanced tests to verify localized titles and descriptions for problem types.
- Added new messages for various problem types and their descriptions.
- Refactored `TypeCard`, `TypeRow`, and `SelectTypeModal` components to use localized strings.
- Improved test coverage for problem type selection and rendering.
2025-08-05 15:37:18 +05:00
Navin Karkera
915bd559e0 feat: disable drag handles for children of library imported containers in course outline [FC-0097] (#2311)
* Hides/disables drag handles for children components of containers imported from library.
* Disables move, delete and duplicate options for children components.
* Move up and down option skips containers that cannot accept children (imported from library).
* Authors cannot drag and drop xblocks under containers imported from library.
* Improves drag-n-drop by creating a representational drag overlay.
2025-08-04 11:46:39 -05:00
Ishan Masdekar
00ce3d7856 feat: navigates subsection breadcrumb to first unit page (#2329)
- navigates the breadcrumb to the first unit under the subsection
instead of the outline page.

Closes #1924
2025-07-31 14:26:18 -07:00
Kyle McCormick
90ddc5e71c chore: Delete CODEOWNERS (#2347)
See: https://github.com/openedx/axim-engineering/issues/1511
2025-07-31 16:18:31 -04:00
renovate[bot]
9ceebbe137 chore(deps): update dependency @edx/frontend-component-header to v6.5.2 (#2334)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-29 13:19:42 -07:00
Ahtesham Quraish
8326257938 refactor: Replace connect with useSelector() and useDispatch() 1/5 2025-07-29 13:03:04 +05:00
Ahtesham Quraish
30cfd269e2 refactor: Replace of injectIntl with useIntl() part 5 #2285 2025-07-29 10:08:31 +05:00
renovate[bot]
082a1c6510 chore(deps): update dependency @edx/frontend-component-header to v6.5.1 (#2309)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-25 15:09:34 -07:00
renovate[bot]
a146307a4f chore(deps): update dependency @codemirror/view to v6.38.1 (#2303)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-25 14:10:55 -07:00
Jacobo Dominguez
556bb1e56d docs: update readme for AdditionalTranslationsComponentSlot (#2321) 2025-07-25 13:57:08 -07:00
Jorg Are
bd2e2d8655 feat: Add pt-BR to videoTranscriptLanguages (#2242) 2025-07-25 12:47:11 -04:00
dependabot[bot]
11de4022f0 chore(deps): bump on-headers and compression (#2328)
Bumps [on-headers](https://github.com/jshttp/on-headers) and [compression](https://github.com/expressjs/compression). These dependencies needed to be updated together.

Updates `on-headers` from 1.0.2 to 1.1.0
- [Release notes](https://github.com/jshttp/on-headers/releases)
- [Changelog](https://github.com/jshttp/on-headers/blob/master/HISTORY.md)
- [Commits](https://github.com/jshttp/on-headers/compare/v1.0.2...v1.1.0)

Updates `compression` from 1.8.0 to 1.8.1
- [Release notes](https://github.com/expressjs/compression/releases)
- [Changelog](https://github.com/expressjs/compression/blob/master/HISTORY.md)
- [Commits](https://github.com/expressjs/compression/compare/1.8.0...v1.8.1)

---
updated-dependencies:
- dependency-name: on-headers
  dependency-version: 1.1.0
  dependency-type: indirect
- dependency-name: compression
  dependency-version: 1.8.1
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-07-24 17:06:52 -07:00
dependabot[bot]
9caa4351ba chore(deps): bump form-data from 4.0.2 to 4.0.4 (#2308)
---
updated-dependencies:
- dependency-name: form-data
  dependency-version: 4.0.4
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-07-24 16:54:00 -07:00
Muhammad Qasim Gulzar
74e671d08b fix: Course outline — issue when editing Section/Subsection/Unit name, and executing an action on the page (#2275)
When editing the title of a section, subsection or unit name, if someone created a new subsection, it would be duplicated and created twice. This change filters out the duplicate entry and eliminates the issue.
2025-07-24 12:40:48 +05:30
Navin Karkera
5b7efc65bc feat: add library reuse indicators to all components in course outline [FC-0097] (#2295)
Add library reuse indicator to units, subsections and sections in course outline.
2025-07-23 13:13:28 -05:00
Ahtesham Quraish
f9cd15eee6 fix: press Cancel it's not properly refreshing the unit and not showing the blank component was added. 2025-07-23 14:13:06 +05:00
Jacobo Dominguez
bad66caadd docs: update readme for additional course plugin slot (#2315) 2025-07-22 13:42:48 -07:00
Jacobo Dominguez
2ae594f23c refactor: replacing injectIntl with useIntl part 4 (#2301) 2025-07-22 09:49:33 -07:00
renovate[bot]
8e3ea89339 chore(deps): update dependency axios-mock-adapter to v2 (#2304)
* chore(deps): update dependency axios-mock-adapter to v2

* test: update tests for compatibility with axios-mock-adapter v2

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Braden MacDonald <braden@opencraft.com>
2025-07-21 23:27:28 +00:00
Navin Karkera
537b3292ee feat: library section subsection reuse in course (#2279)
Adds option for course author to import and use sections and subsections from library v2.
2025-07-21 15:38:47 -05:00
Ahtesham Quraish
46d5917303 refactor: Replace of injectIntl with useIntl() part 7 (#2297) 2025-07-18 14:52:57 -07:00
Jacobo Dominguez
4f3904ea4c refactor: replacing injectIntl with useIntl part 3 (#2300) 2025-07-18 14:47:18 -07:00
Ahtesham Quraish
215f7280da refactor: Replace of injectIntl with useIntl() part 6 (#2298) 2025-07-18 14:37:03 -07:00
Jacobo Dominguez
60417a76cb refactor: replacing injectIntl with useIntl part 2 (#2299) 2025-07-18 14:21:39 -07:00
Jacobo Dominguez
cbec6505f5 refactor: replacing injectIntl with useIntl part 1 (#2296) 2025-07-18 14:21:26 -07:00
Diana Villalvazo
654daa58ee fix: Change "choose library" phrasing depending on contentType (#2276)
* fix: change library dialog phrasing depending on contentType

* fix: separate i18n messages

* fix: i18n improvements
2025-07-17 16:20:24 -07:00
Braden MacDonald
bd18e874b5 Fix broken StudioHome tests (#2291)
There were a ton of problems with these tests, but the main one was the use of `waitFor` without `await`, causing all of the code inside each `waitFor` block to essentially be ignored.

Other problems fixed:
* Rendering a router inside a router was causing most of the render() calls to fail (our custom `render()` already provides a router so there's no need to provide one in the test case)
* Use of `testid` instead of queries based on what users see
* Tests could match on content in the body when trying to make assertions about the header
* Mock imported via `index.js` was causing `jest-haste-map` to print warnings about duplicate mock names (this is still happening for other mocks)
* Passing `courses: null` instead of `courses: []` was causing a broken render on two of the tests.

I also made other cleanups to follow best practices.
2025-07-17 15:45:22 -05:00
jacobo-dominguez-wgu
2db6d89fca test: upgrading user-event to v14 (#2277) 2025-07-17 09:26:16 -07:00
Ahtesham Quraish
a51ff99042 fix: Files and Uploads search text is lost when using Sort and Filter more than once 2025-07-17 10:36:34 +05:00
renovate[bot]
966767ffd4 chore(deps): update dependency react-select to v5.10.2 (#2273)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-14 14:32:29 -07:00
renovate[bot]
fc9ded432a chore(deps): update dependency @edx/frontend-component-header to v6.4.2 (#2272)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-14 14:30:29 -07:00
Muhammad Faraz Maqsood
a3e03dc12f Revert "Remove unused <EditorContainer> and URL route" (#2274) 2025-07-14 12:10:13 -04:00
Devasia Joseph
77fe2d1086 feat: Update Course Optimizer label from BETA to NEW 2025-07-11 19:03:30 +05:00
jacobo-dominguez-wgu
efd967a42b test: replacing shallow snapshots with RTL part 2 (#2267) 2025-07-10 16:49:16 -07:00
jacobo-dominguez-wgu
eaba380417 test: replacing shallow snapshots with RTL (#2266) 2025-07-10 16:47:20 -07:00
Diana Villalvazo
f3332a214f fix: remove old editorContainer (#2268) 2025-07-10 16:44:41 -07:00
jacobo-dominguez-wgu
bc11aaf5ce test: removing @edx/react-unit-test-utils library (#2263) 2025-07-08 13:27:36 -07:00
jacobo-dominguez-wgu
4ae9d3c8df test: replacing snapshot tests with RTL tests part 16 (#2252) 2025-07-08 10:08:32 -07:00
jacobo-dominguez-wgu
3673c5f561 test: replacing snapshot tests with RTL tests part 14 (#2236) 2025-07-08 09:37:07 -07:00
jacobo-dominguez-wgu
2fe24f39ac test: replacing snapshot tests with RTL tests part 13 (#2233) 2025-07-08 09:33:53 -07:00
jacobo-dominguez-wgu
c4f565bf76 test: replacing snapshot tests with RTL tests part 15 (#2248) 2025-07-07 16:44:01 -07:00
Braden MacDonald
e8e5a3c4ce docs: update PR template (#2215)
* docs: update PR template

* docs: incorporate suggestions from PR review
2025-07-07 23:04:58 +00:00
renovate[bot]
749c0022ec chore(deps): update dependency @edx/frontend-component-header to v6.4.1 (#2245)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-07 22:47:12 +00:00
Daniel Valenzuela
642be162d7 fix: open component with keyboard within unit page (#2256)
When selecting a component inside a unit with the keyboard, opening it in the sidebar by pressing enter was not working.
2025-07-07 17:34:21 -05:00
jacobo-dominguez-wgu
a969de4d90 test: replacing snapshot tests with RTL tests part 12 (#2222) 2025-07-07 15:25:27 -07:00
renovate[bot]
8d86433748 chore(deps): update dependency codemirror to v6.0.2 (#2240)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-07 22:12:46 +00:00
renovate[bot]
90f375939a chore(deps): update codemirror sub-packages (#1908)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-07 15:12:22 -07:00
renovate[bot]
a1214f7fa9 chore(deps): update react-router monorepo to v6.30.1 (#2156)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-07 14:57:24 -07:00
renovate[bot]
88821077cb chore(deps): update dependency @openedx/paragon to v23.14.0 (#2191)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-07 21:56:17 +00:00
renovate[bot]
3ebd2372d2 chore(deps): update dependency @tanstack/react-query to v4.40.1 (#2260)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-07 21:55:52 +00:00
renovate[bot]
6014da9a22 chore(deps): update dependency @types/lodash to v4.17.20 (#2259)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-07 14:42:36 -07:00
edX requirements bot
f355a17943 chore: update browserslist DB (#2258)
Co-authored-by: abdullahwaheed <42172960+abdullahwaheed@users.noreply.github.com>
2025-07-07 00:22:40 +00:00
Daniel Valenzuela
ae82486d72 fix: cannot open item sidebar after closing with 'X' button (#2241)
When closing an item (component, unit, section, container, etc) sidebar, and trying to reopen it inmediately by clicking the card, it was not opening because navigateTo was being used, but the URL already was the same you were being navigated to. So we also have to update the sidebar item info in the sidebar context in order for it to reopen properly.
2025-07-04 04:38:37 -05:00
Muhammad Faraz Maqsood
dd2853900b feat: apply 2 column format for P&R page widgets 2025-07-04 10:40:15 +05:00
Rômulo Penido
2533724203 fix: increase sidebar width to prevent menu overflow (#2247) 2025-07-03 03:27:01 -05:00
Chris Chávez
ffd430d321 fix: Update add content existing content icons in section/subsection pages (#2224) 2025-07-03 02:36:19 -05:00
Muhammad Faraz Maqsood
ad62519af7 fix: publish btn doesn't show after component edit
When we edit & save the component, publish button doesn't show up until we refresh the page manualy or open this unit by opening previous unit and coming back to this unit again.
In this commit, we are dispatching a storage event whenever we edit the component, it'll refresh the page & show the publish button as expected.
2025-07-01 15:19:39 +05:00
Daniel Valenzuela
76b5dd5925 fix: delete and remove modals styling (#2243) 2025-06-30 15:49:25 -05:00
Navin Karkera
701e41b664 fix: invalidate library queries on restore container (#2231)
Restoring containers should invalidate queries in the library to show the deleted component without needing to refresh.
2025-06-30 15:30:48 -05:00
Rômulo Penido
ac9faacc4d feat: add ParentBreadcrumbs component [FC-0090] (#2223)
Adds the `ParentBreadcrumbs` component to show a list of parent containers on the Unit and Subsection breadcrumbs.
2025-06-30 12:30:48 -05:00
renovate[bot]
37313b30bd chore(deps): update dependency @types/lodash to v4.17.19 (#2239)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-30 01:25:47 -07:00
edX requirements bot
6f79e4475d chore: update browserslist DB (#2238)
Co-authored-by: abdullahwaheed <42172960+abdullahwaheed@users.noreply.github.com>
2025-06-30 00:22:35 +00:00
Daniel Valenzuela
e2da13d129 feat: remove unit/subsection from subsection/section (#2149)
Let the user remove a unit/subsection from a subsection/section.
2025-06-27 08:08:40 -05:00
Daniel Valenzuela
aeefcc639f fix: open section/unit with keyboard inside section/subsection page (#2209)
When selecting a unit/subsection inside a subsection/section with the keyboard, opening it in the sidebar by pressing enter was not working.
2025-06-25 12:06:33 -05:00
Navin Karkera
4905f3bbc7 feat: container delete confirmation modal (#2145)
Update container delete confirmation modal based on #1982 and #1981
2025-06-24 12:37:14 -05:00
Brayan Cerón
60cebf703d fix: clear selection on files & uploads page after deleting (#2056)
* refactor: remove selected rows when deleting or adding elements

* refactor: ensure unique asset IDs when adding new ones

* refactor: remove unnecessary loading checks in mockStore function

* test: add unit tests for TableActions component
2025-06-24 08:19:36 -07:00
jacobo-dominguez-wgu
71fa247c61 test: replacing snapshot tests with RTL tests part 11 (#2214) 2025-06-24 03:04:03 -07:00
renovate[bot]
31fc0453b4 chore(deps): update dependency @types/react to v18.3.23 (#2212)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-24 10:03:43 +00:00
renovate[bot]
75137bc651 chore(deps): update dependency @types/lodash to v4.17.18 (#2211)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-24 10:03:31 +00:00
jacobo-dominguez-wgu
085cd7d05c test: replacing snapshot tests with RTL tests part 10 (#2207) 2025-06-24 02:50:54 -07:00
Muhammad Faraz Maqsood
810dd420fd fix: course optimizer styling 2025-06-24 12:51:41 +05:00
Jillian
747f7b6133 Add subsection/unit to section/subsection full-page view within library [FC-0090] (#2152)
* Removes the "content type" tab bar from the "Add Existing Component/Unit/Subsection" modal, and in all cases where only one type of content is shown for selection.
* Updates the section/subsection sidebar to show "Existing Library Content" + "New Subsection" / "New Unit" buttons.
* Updates the "Add New Unit/Subsection" buttons to directly launch the new container modal, instead of going via the container sidebar.
* Ensures that whenever a subsection/unit is created from within a section/subsection, that it is linked to the parent section/subsection after created.
2025-06-24 02:05:35 -05:00
Chris Chávez
ebf4b7c162 feat: Open Manage tags on click tag count in section/subsection page [FC-0090] (#2136)
Add functionality to open the manage tags when clicking the tag count in the section/subsection page.
2025-06-23 12:11:09 -05:00
jacobo-dominguez-wgu
962b30bed9 test: replacing snapshot tests with RTL tests part 9 (#2206) 2025-06-20 12:05:34 -07:00
Chris Chávez
75ea7500e1 fix: Rename optimistic update in children containers (#2141)
Fix: When you update the title of a unit/subsection in the subsection/section page, the text returns to the previous value for a while
2025-06-19 16:11:13 -05:00
jacobo-dominguez-wgu
08c3d123d8 test: replacing snapshot tests with RTL tests part 7 (#2181) 2025-06-19 09:11:51 -07:00
Devasia Joseph
920f4a54e1 fix: TNL-10093 Removed inaccurate course rerun message 2025-06-19 14:49:50 +05:00
renovate[bot]
d9e1a4dea6 chore(deps): update dependency @openedx/frontend-build to v14.6.1 (#1561)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-18 15:31:30 -07:00
renovate[bot]
8b9a80eb04 chore(deps): update dependency @tanstack/react-query to v4.40.0 (#2155)
* fix(deps): update dependency @tanstack/react-query to v4.40.0

* fix: npm was trying to install react-native which conflicted with React 18

The problem is that react-query 4.40.0 specifies an optional, unpinned peerDependency on react-native, and then it depends on @types/react 19, causing a conflict with our React 18. Putting an explicit dependency on the React types solves this. As would upgrading to React Query v5.

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Braden MacDonald <braden@opencraft.com>
2025-06-18 22:12:04 +00:00
jacobo-dominguez-wgu
a5c17452e7 test: replacing snapshot tests with RTL tests part 8 (#2184) 2025-06-18 15:09:21 -07:00
renovate[bot]
4b4ab92383 chore(deps): update dependency redux to v4.2.1 (#1936)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-18 21:51:17 +00:00
Chris Chávez
97710c262e fix: Truncated text in InplaceTextEditor [FC-0090] (#2146)
* Fix the truncated text in InplaceTextEditor.
* Fix the truncated text in the breadcrumbs in the subsection page
2025-06-18 21:38:17 +00:00
Muhammad Faraz Maqsood
b510b6f69f fix: update advanced module list not working (#2189)
Backend was still expecting `{'advanced_modules', {'value': ['poll', 'problem-builder', 'h5pxblock']}}` but without this change, it was receiving `{'advancedModules', ['poll', 'problem-builder', 'h5pxblock']}`

Follow up to https://github.com/openedx/frontend-app-authoring/pull/1581

Co-authored-by: Muhammad Faraz  Maqsood <faraz.maqsood@A006-01130.local>
2025-06-18 13:33:03 -07:00
Kyle McCormick
488173ebdb fix: Subsections should come before Sections 2025-06-18 12:19:50 -07:00
Brian Smith
5a84d8c52f feat!: add design tokens support (#2187)
BREAKING CHANGE: Pre-design-tokens theming is no longer supported.

Co-authored-by: Diana Olarte <diana.olarte@edunext.co>
2025-06-18 15:17:18 -04:00
Daniel Valenzuela
ac7f90065d feat: create subsections, units from within containers (#2104)
Functionality to create subsections, and units from within sections, and subsections respectively.
2025-06-18 12:58:03 -05:00
jacobo-dominguez-wgu
19f81cc05d test: replacing snapshot tests with RTL tests part 6 (#2173)
* test: replacing snapshot tests with rtl tests part 6

* fix: removing testing purposed text

* test: fixing test mocking issues
2025-06-17 12:29:34 -07:00
Jillian
fa9d66c5e5 fix: show "This <containerType> is empty" (#2157)
Shows "This is empty" text when container child list is empty for units, subsections, and sections.
2025-06-16 17:09:55 -05:00
renovate[bot]
dc16b226f0 chore(deps): update dependency @openedx/paragon to v22.20.2 (#2150)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-16 17:12:41 +00:00
jacobo-dominguez-wgu
cba4e684ab test: replacing snapshot tests with RTL tests part 5 (#2143)
* test: replacing snapshot tests with rtl tests part 5

* test: removig extra tests

* test: snaps update

* test: adding import shorthand and turning tests into ts

* docs: clarify which line the comment is about

---------

Co-authored-by: Braden MacDonald <braden@opencraft.com>
2025-06-16 16:56:59 +00:00
renovate[bot]
96df339be5 chore(deps): update dependency @types/lodash to v4.17.17 (#2154)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-16 09:54:23 -07:00
Muhammad Faraz Maqsood
eaee5257bd fix: disable tolerance for multiple answers
As tolerance was being only applied to first correct answer. So, disable tolerance and do not apply it in case of multiple correct answers for Numerical input problem type according to the given documentation: https://docs.openedx.org/en/latest/educators/how-tos/course_development/exercise_tools/manage_numerical_input_problem.html#add-a-tolerance:~:text=hints%20to%20problems.-,Add%20Multiple%20Correct%20Responses%20via%20the%20Advanced%20Editor,text%20string%20as%20correct%20answers.
2025-06-16 11:10:57 +05:00
edX requirements bot
154b411ad8 chore: update browserslist DB (#2153)
Co-authored-by: abdullahwaheed <42172960+abdullahwaheed@users.noreply.github.com>
2025-06-16 00:22:22 +00:00
José Ignacio Palma
284e9c7d68 fix: advanced-settings api should not camel-case return value (#1581) 2025-06-13 23:54:32 +00:00
bydawen
fcf1e5cb33 fix: Text truncate issue in the search modal (#2137) 2025-06-13 16:28:29 -07:00
jacobo-dominguez-wgu
2e9b5b7e78 test: replacing snapshot tests with RTL tests part 3 (#2134)
* test: replacing snapshot tests with rtl tests part 3

* test: addint alt text to icon buttons and test refactor
2025-06-13 09:05:48 -07:00
renovate[bot]
8a423ebf10 chore: update dependency yup to v0.32.11 (#1937) 2025-06-12 15:50:38 -07:00
renovate[bot]
b6db457c6f chore: update dependency @edx/browserslist-config to v1.5.0 (#1562) 2025-06-12 22:47:12 +00:00
renovate[bot]
80dabca88e chore: update dependency @edx/frontend-platform to v8.4.0 (#2086)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-12 15:32:22 -07:00
renovate[bot]
376414a653 chore: update dependency @openedx/paragon to v22.20.1 (#2023)
* fix(deps): update dependency @openedx/paragon to v22.20.1

* fix: minor type warnings from new Paragon version

* test: update snapshot test

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Braden MacDonald <braden@opencraft.com>
2025-06-12 15:29:01 -07:00
jacobo-dominguez-wgu
ca85ca8e4b test: replacing snapshot tests with RTL tests part 4 (#2135)
* test: replacing snapshot tests with rtl tests part 4

* test: removing not needed icon mocks, and changing name to render function for editors
2025-06-12 14:12:28 -07:00
Braden MacDonald
4a1f454855 fix: searching "all courses" from studio home wasn't working (#2083) 2025-06-12 09:25:31 -07:00
Chris Chávez
4adf2ff087 fix: Refresh section list on subsection page (#2103)
Invalidates the query in the subsection page used to get the list of sections that contains the subsection
2025-06-12 00:57:25 +00:00
Chris Chávez
569a981a85 fix: show unit published name in sidebar on content picker (#2100)
Fix the bug for show unit published name in sidebar on content picker.
2025-06-12 00:48:56 +00:00
jacobo-dominguez-wgu
3097976b7b test: replacing snapshot tests with RTL tests part 2 (#2132) 2025-06-11 10:15:59 -07:00
Diana Villalvazo
acef2e70cc fix: remove icon and empty breadcrumb from libraries (#2129) 2025-06-11 09:36:45 -07:00
Jillian
c1d874f94f fix: show Preview tab in sidebar when container child selected [FC-0090] (#2128)
Shows the sidebar Preview tab when a container child is selected, while hiding the Preview tab when sidebar shows the container itself, since it's "preview" is already in the main page body.

Adds tests to ensure this behavior is maintained.
2025-06-10 20:05:11 -05:00
jacobo-dominguez-wgu
dc16c42746 test: replacing snapshot tests with RTL tests part 1 (#2108)
* test: replacing snapshot tests with rtl tests part 1

* test: using screen to query and refactor some tests
2025-06-10 15:09:19 -07:00
Ihor Romaniuk
ea33c15b36 fix: remove an extra editing xblock modal on unit page (#2111) 2025-06-10 10:11:01 -07:00
Muhammad Anas
d440394067 fix: remove duplicate markdown_edited save request (#2112)
Removes the unnecessary duplicate save  request of markdown_edited
value to the backend.

Part of: https://github.com/openedx/frontend-app-authoring/issues/2099
Will be backported to Teak.
2025-06-10 11:59:23 -04:00
Arunmozhi
73ac6d725a feat: add v2 CourseAuthoringUnitSidebarSlot (#2000) 2025-06-10 11:46:27 -04:00
Chris Chávez
0e2cab2838 fix: Issue on the in-place editor when renaming library containers (#2101)
* fix: Rename icon size & make the button disappear while editing

* style: Use the correct type in useUpdateContainer
2025-06-09 13:48:51 -07:00
Jillian
b3605fa1b8 refactor: make the unit sidebar code work for any type of container [FC-0090] (#2066)
Refactors the library sidebar and unit info code to make it work for subsections and subsections too
2025-06-09 17:28:58 +00:00
Chris Chávez
be13c18e5d feat: Section/Subsection Card Preview [FC-0090] (#2057)
Section/Subsection card previews
2025-06-09 11:10:18 -05:00
edX requirements bot
019eede7c2 chore: update browserslist DB (#2105)
Co-authored-by: abdullahwaheed <42172960+abdullahwaheed@users.noreply.github.com>
2025-06-09 00:22:11 +00:00
Braden MacDonald
5991fd3997 refactor: Load waffle flags using React Query (#2068)
* refactor: use React Query to load waffle flags

* test: add test case

* fix: more clear handling of data loading and fallbacks

* refactor: simplify handling of useReactMarkdownEditor

* test: use new mockWaffleFlags() helper

* test: simplify test mocks in hooks.test.js

* refactor: avoid duplicating flag names, clarify how defaults work
2025-06-05 11:02:57 -07:00
Ihor Romaniuk
9a2dac6d4b fix: load sequences in unit page (#1867)
This handles loading errors when opening the course unit page via direct link as an unauthorized user.
2025-06-05 09:39:43 -03:00
jacobo-dominguez-wgu
061855c31e feat: update to latest footer component, remove broken links (#2072) 2025-06-04 16:17:13 -07:00
diana-villalvazo-wgu
5df4cd941d fix: refactor best practices checklist logic (#2038)
The Best Practices Checklist behavior was wrong for some cases:

* Video: if duration is null it shouldn't be marked as completed
* Video: if there are no videos it shouldn't be marked as completed
* Unit depth: if course doesn't have units it shouldn't be marked as completed
* Diverse learning sequence: description mentions that 80% should contain multiple content types, so if it is exactly 80% it should be marked as completed
2025-06-04 16:46:01 -03:00
Adolfo R. Brandes
7274316eb8 chore: Remove extraneous config file 2025-06-04 16:31:32 -03:00
diana-villalvazo-wgu
151b3e30bf fix: files & uploads menu was truncated due to overflow-x (#2071) 2025-06-04 10:45:22 -07:00
Navin Karkera
dd6780ff41 feat: library section and subsection page (#2032)
* Adds section and subsection library pages. 
* Refactors routing to support them and fix routing from collections page to other pages.
* Refactors library context to reliably set component, unit, and other container ids even when the url changes when user goes back in history rapidly.
2025-06-04 17:32:29 +00:00
Muhammad Anas
99e11d3534 fix: markdown editor issues in modal (#2074)
This PR resolves rendering issues with the Markdown editor inside the modal.
The problem began after a PR [1] introduced the use of modals for the editor.
The EditorPage [2] component expects a `isMarkdownEditorEnabledForCourse` prop,
which was missing in that implementation.

[1] https://github.com/openedx/frontend-app-authoring/pull/1838 
[2] https://github.com/openedx/frontend-app-authoring/pull/1838/files#diff-147218ef88726880178ea895988a5d3feaf2c0c4459086a8de7a4080cbe37de7R226
2025-06-04 12:13:55 -04:00
Victor Navarro
ee7992bde5 fix: Expand all now expands subsections (#1998)
* fix: Expand all now expands subsections

* fix: test cases
2025-06-04 13:02:41 -03:00
Braden MacDonald
1dee2bba58 test: refactor many test suites to use testUtils/initializeMocks (#2067)
Simplifies a bunch of test code by converting it to use our handy testUtils module.

None of the app code is change, just test code. And none of the test cases are modified in any meaningful way - this just simplifies the setup/cleanup significantly.
2025-06-03 15:52:56 -05:00
dependabot[bot]
d806b6150d chore(deps): bump tar-fs (#2069)
Bumps  and [tar-fs](https://github.com/mafintosh/tar-fs). These dependencies needed to be updated together.

Updates `tar-fs` from 2.1.2 to 3.0.9
- [Commits](https://github.com/mafintosh/tar-fs/compare/v2.1.2...v3.0.9)

Updates `tar-fs` from 3.0.8 to 3.0.9
- [Commits](https://github.com/mafintosh/tar-fs/compare/v2.1.2...v3.0.9)

---
updated-dependencies:
- dependency-name: tar-fs
  dependency-version: 3.0.9
  dependency-type: indirect
- dependency-name: tar-fs
  dependency-version: 3.0.9
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-06-03 10:12:59 -07:00
Muhammad Faraz Maqsood
c7f3e26798 feat: keep content inside the card 2025-06-03 15:11:04 +05:00
Muhammad Faraz Maqsood
27a2b1235e feat: align icons and re-design sections layout
- TNL-11990: Icons Alignment
- TNL-11982: Re-design Sections layout
2025-06-03 15:11:04 +05:00
Muhammad Faraz Maqsood
3c69733170 feat: add sections filter & UI changes
- TNL-11973: Previously Filters functionality was only working for subsections and units inside sections. Now sections are also filtered.
- TNL-11974: New request, Show "no results found" if no results match the filters
- TNL-11975: UI Change, Align filter menu popup to left side of filter button
- TNL-11976: UI Change, Remove underline below "Course optimizer" title
- TNL-11978: UI Change, Change title to "Scan my course"
- TNL-11989: UI Change, Use empty space to display link, don't truncate text before the space runs out
- TNL-11977: New request, Remove this stuff(scanning steps) when scan is complete, it'll disappear after 2.5 seconds
- TNL-11979: UI Change, Move "This tool will scan your course..." text inside of Scan card
- TNL-11980: UI Change, Move "Last scanned on..." date text below Scan button
- TNL-11981: UI Change, Remove icon from "Start scanning" button
- TNL-11983: UI Change, "Start scanning" button should be smaller, made it medium sized
- TNL-11984: UI Change, Remove dividing line under subsection name in expanded card
- TNL-11985: UI Change, Fix alignment of dividing lines, links, and icons in expanded cards to match Figma.
- TNL-11986: UI Change, Match color of the broken icon with other Icons
- TNL-11987: UI Change, Fix alignment of Filter chips to match Figma
- Also added Beta Badge for course optimizer.
- Added tests for codecov coverage
2025-06-03 15:11:04 +05:00
Rômulo Penido
cfb4944d43 feat: refactor library routes and add section/subsection tabs (#2039)
Adds the "Section" and "Subsections" tabs to the library authoring and refactors our library router hook to fix some ambiguities and solve some bugs.
2025-06-02 14:05:31 -05:00
Tony Busa
17e514f937 Add html lang prop to CodeEditor Component inside the SourceCodeModal (#2043) 2025-06-02 09:18:38 -04:00
Muhammad Faraz Maqsood
0c7cef66ab feat: fix toolbar of text editor at top while scrolling 2025-06-02 09:40:33 +05:00
edX requirements bot
c677e7fef3 chore: update browserslist DB (#2058)
Co-authored-by: abdullahwaheed <42172960+abdullahwaheed@users.noreply.github.com>
2025-06-02 00:21:33 +00:00
diana-villalvazo-wgu
50cb8608c4 feat: add library name to review updates block card (#2037) 2025-05-30 22:04:02 +00:00
Brayan Cerón
5bb8a5d47c refactor: remove references of ENABLE_HOME_PAGE_COURSE_API_V2 (#1611)
* refactor: remove references of ENABLE_HOME_PAGE_COURSE_API_V2
* fix: infinite requests when clearing filters
* fix: some requests were being duplicated when changing filters
* test: adapt tests to the latest changes
* test: improve test coverage
* refactor: drop tab for archived courses
* test: filter reset functionality in CoursesTab component
* refactor: revert deletion of isShowProcessing
* test: update visibility check for pagination text in TabsSection tests
* refactor: update dropdown and button accessibility in CardItem and CoursesTab components
2025-05-30 14:31:47 -07:00
Braden MacDonald
f02347dd71 perf: only import JSZip when needed (bundle splitting) (#1933) 2025-05-30 09:45:20 -07:00
Maria Grimaldi
0eaa7f6f88 fix: add file extension to .yml 2025-05-30 08:31:53 -04:00
Maria Grimaldi
e6c1c95260 refactor: use workflow from .github repo 2025-05-30 08:13:39 -04:00
Maria Grimaldi
a18444e691 chore: add workflow to pull release testing issues into the BTR board 2025-05-30 08:13:39 -04:00
Maria Grimaldi
5561c030e8 chore: add workflow to pull release testing issues into the BTR board
Add GH workflow that includes issues into the BTR board after the issue
is labeled with `release testing`.  Also add label needs triage for
bug triaging issues.
2025-05-30 08:13:39 -04:00
Rômulo Penido
fffa9e2566 fix: selection card wiggle (#2045)
Removes the library cards' resize because of the border change on selection.
2025-05-29 18:54:02 +00:00
Rômulo Penido
f18274533e fix: set unit preview readonly on sidebar (#2008)
Make the unit preview on the sidebar read-only and add `Truncate` to the `InplaceTextEditor`
2025-05-29 13:33:23 -05:00
Jillian
3fc0f27d67 fix: upstreamInfo is not always provided (#2041) 2025-05-29 10:58:20 -05:00
sundasnoreen12
36e57a0cfb fix: passed course id and fixed test cases 2025-05-29 08:53:47 +05:00
Navin Karkera
afd6afdbb9 feat: create section and subsection in library (#2013)
Adds create section and subsection buttons in sidebar.
2025-05-28 16:10:17 -05:00
Braden MacDonald
01243afdd9 chore: bump UUID to v11 (#1932) 2025-05-27 13:08:24 -05:00
diana-villalvazo-wgu
b0e194e512 fix: Display "details" tab as default in collections (#2027) 2025-05-26 15:59:30 -07:00
Daniel Valenzuela
260582b6f0 fix(search-manager): send search keywords to facet search too (#2020)
Filter counts depends on the response for the meilisearch faceted search. Since the search keywords affect the search results, in order to make the block type filter counts reflect the search results, we need to include the search keywords into the faceted search too.
2025-05-26 13:54:54 -05:00
sundasnoreen12
ce337aedef test: added test case 2025-05-26 13:31:04 +05:00
sundasnoreen12
44d47f8783 fix: removed commented line 2025-05-26 13:31:04 +05:00
sundasnoreen12
65c8b8ba4b test: fixed test case issue 2025-05-26 13:31:04 +05:00
sundasnoreen12
80dba704da fix: fixed spacing issues 2025-05-26 13:31:04 +05:00
Ahtisham Shahid
9906901262 fix: updated failing tests 2025-05-26 13:31:04 +05:00
Ahtisham Shahid
5167b167eb feat: added process to sync discussions topic on page load
feat: added process to sync discussions topic on page load

fix: updated failing unit tests

feat: added course creation date in index api

feat: added course creation date in index api

fix: updated unit tests
2025-05-26 13:31:04 +05:00
Muhammad Faraz Maqsood
951b707c7d fix: increase max value for the grace period
- increase max value for the grace period as same as it was before in the legacy experience
2025-05-26 11:03:46 +05:00
dependabot[bot]
8e1e2fdb46 build(deps): bump codecov/codecov-action from 4 to 5
Bumps [codecov/codecov-action](https://github.com/codecov/codecov-action) from 4 to 5.
- [Release notes](https://github.com/codecov/codecov-action/releases)
- [Changelog](https://github.com/codecov/codecov-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/codecov/codecov-action/compare/v4...v5)

---
updated-dependencies:
- dependency-name: codecov/codecov-action
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-05-26 10:51:26 +05:00
edX requirements bot
ca8ce5b253 chore: update browserslist DB (#1997)
Co-authored-by: abdullahwaheed <42172960+abdullahwaheed@users.noreply.github.com>
2025-05-26 00:21:21 +00:00
Jillian
c5f7d0cf3b fix: set maxHeight on TextEditor TinyMce widget [FC-0090] (#2024)
Sets a max_height=500px for the TinyMCE editor when editing a Text/Html component.
This prevents the autoresize plugin from expanding the editor textarea beyond the bounds of the editor modal.

⚠️ Because the max height can only be a numeric pixel value, we can't use clever settings like vh or %, and so we're forced to limit the height of the editor to a fixed size for all screen sizes in order to address this issue.
2025-05-23 13:51:39 -05:00
Jillian
ac5574d2c4 fix: refresh xblock inline after accepting/rejecting library sync (#2022)
Instead of reloading the entire Unit after syncing changes from the
library, just reload the xblock that was changed.
2025-05-22 10:49:23 -05:00
Muhammad Faraz Maqsood
df3577241f fix: studio time zone conversion issue 2025-05-22 10:32:41 +05:00
Chris Chávez
65605bf937 fix: Remove never published filter from component picker (#1947)
Removes the never-published filter option from the component picker and unit picker.
2025-05-22 00:37:42 +00:00
Rômulo Penido
9e978057bc fix: rename library publish button to "Publish All" (#2016)
renames the Library publish button from "Publish All Changes" to "Publish All"
2025-05-21 18:12:17 -05:00
diana-villalvazo-wgu
279a900a10 fix: rename "updates" and "libraries" menu items for clarity (#2004) 2025-05-21 11:46:29 -07:00
jacobo-dominguez-wgu
f2d5bc4680 fix: removing edX text reference on video fallback message (#1996) 2025-05-21 11:41:58 -07:00
Chris Chávez
ee5e51d371 fix: Inconsistent publish status filter menu placement (#1966) 2025-05-21 10:22:34 -07:00
Chris Chávez
0e40aa295d feat: New confirmation modal when deleting library components (#1906)
* feat: New units message on delete confirm modal when deleting components
* test: Tests for new delete confirmation messages
* refactor: useComponentsFromSearchIndex added
* refactor: Move fetch units to ComponentDeleter
* style: Delete unnecessary code
2025-05-21 10:04:19 -07:00
Rômulo Penido
e212e1a1ef fix: rename library publish button (#1993)
This PR renames the Library publish button from "Publish" to "Publish All Changes"
2025-05-20 18:22:51 -05:00
Daniel Valenzuela
1568067980 fix: do open editor of new xblock when duplicating (#1887)
Fixes bug where after duplicating an xblock, the editor modal of the old xblock is being open instead of the new copied xblock.
2025-05-20 17:51:42 -05:00
Navin Karkera
503642be8c feat: open collection or unit page on double click only (#2002)
Opens collection or unit page only on double click.
2025-05-20 15:34:03 -05:00
Navin Karkera
6f3b7ab962 fix: search text flickering (#1999)
Fix flickering issue in search field.
2025-05-19 12:30:16 -05:00
Navin Karkera
08ac1c0c4d feat: select component and show sidebar on edit (#1949)
Select component that is being edited in library and show its sidebar. Also fixes issue with children component listing in library unit page
2025-05-19 12:24:14 -05:00
Rômulo Penido
6a3b0064ff fix: change InplaceTextEditor style and add optimistic update (#1953)
* Optimistic update for renaming Components, Collections and Containers
* Change the InplaceTextEditor to show the new text until the onSave promise resolves
* Change the InplaceTextEditor style to: Always show the rename button
2025-05-19 11:46:54 -05:00
renovate[bot]
1d45fa2e38 fix(deps): update dependency @edx/frontend-platform to v8.3.7 (#1469)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-19 09:40:50 -04:00
Muhammad Faraz Maqsood
62bffc06d7 feat: add never show assessment msg in subsection
- add never show assessment result message in subsection card
2025-05-19 09:06:50 +05:00
jacobo-dominguez-wgu
88aa4c1524 fix: various additional i18n issues
* fix: adding messages for i18n issues related to aria labels

* fix: replacing ternary operator

* refactor: injecting intl object to prevent extra modifications on license details component

* test: fixing failing snapshot test

* chore: best descriptions to aria label and removing aria label not needed

* test: snapshot update and fixing ut

* chore: best descriptions to aria labels
2025-05-13 20:14:14 +00:00
Braden MacDonald
4ebc1590e7 docs: update maintainer to me in catalog-info.yaml
https://discuss.openedx.org/t/frontend-app-authoring-maintanership-transfer/15786
2025-05-13 13:14:05 -07:00
Navin Karkera
97e5fbaa5e chore: add comment to prevent course search reload on type 2025-05-13 11:34:27 -07:00
Ivo Branco
03a757de21 fix: include plugins in i18n extraction script (#1802)
Add the additional source `plugins` folder on the `fedx-scripts` extract translations.
This can be configured because the frontend-build v14.5.0 now supports additional source folders using the `--include` extra argument.
2025-05-13 12:56:11 -04:00
Braden MacDonald
d8eda2494b chore: remove unused dependencies "npm" and "start" (#1931) 2025-05-12 15:24:29 -05:00
jacobo-dominguez-wgu
db07092880 fix: various i18n issues
* fix: adding messages for i18n issues related to placeholders

* fix: adding messages for i18n issues related to import tag wizard stepper titles

* fix: changing name to duplicated id on i18n message

* fix: replacing hardcoded string with constants to solve i18n issue

* fix: typo on title prop

* fix: adding components prop name correctly

* test: adding ut for select video modal

* chore: adding description to placeholder, changing extension to constant file and adding uts for code coverage

* chore: removing outdated comment lines
2025-05-12 09:49:03 -07:00
Navin Karkera
26c6a71624 fix: search modal refresh on typing (#1938)
The whole page was being refreshed while searching content from course
outline page due to fetching of waffle flag on changes in location
search field.
2025-05-12 11:42:59 -05:00
Rômulo Penido
d5dc8b5ebe fix: review/sync bugs [FC-0083] (#1905)
Fixes issues related to component libraries' review/sync flow

* Inconsistent sync pane title versions
* Library content shown in preview warning only appears in review changes modal when that modal is opened from the review tab
* Some new changes only appear within library review tab on scroll at top of list
* Vertically misaligned sync icon in review changes message on course outline
* Show available updates whenever content is updated, regardless of number of updates available
2025-05-12 09:57:38 -05:00
Chris Chávez
7b2cc125a5 fix: Issue with read-only units in libraries & published version of units in library units picker [FC-0083] (#1926)
Fixes the issues from https://github.com/openedx/frontend-app-authoring/issues/1633#issuecomment-2828953801

* In successfully added units, the "add new component" widget appears sometimes
* In the "add existing unit" modal, the preview shows draft versions of units
2025-05-12 09:45:46 -05:00
edX requirements bot
9a2dc8061a chore: update browserslist DB (#1935)
Co-authored-by: abdullahwaheed <42172960+abdullahwaheed@users.noreply.github.com>
2025-05-12 00:21:47 +00:00
Rômulo Penido
cc47616256 fix: improve focus/selected style on library authoring (#1918)
Improves the focus and selected styles from the LibraryPage and UnitPage.
2025-05-09 11:05:03 -05:00
Muhammad Faraz Maqsood
e8463f7a6a feat: add more unit tests for code coverage
- add more unit tests for code coverage to cover more use cases
- ignore Modal close in code coverage as our modal does not have close button
2025-05-09 11:28:21 +05:00
Muhammad Faraz Maqsood
c77c4f3c91 feat: add unit tests for filter functionality 2025-05-09 11:28:21 +05:00
Muhammad Faraz Maqsood
36277d8ef5 feat: course optimizer page better design
- Add filter functionality to course optimizer broken links to check different results
- modify design, make use of logo with better tooltip
- change message texts in different area of the page
2025-05-09 11:28:21 +05:00
Braden MacDonald
cdb8016657 fix: invalidate search results when publishing all changes in library (#1925) 2025-05-08 17:28:07 -05:00
Navin Karkera
8c3fab3792 fix: UX issues in unit page (#1913)
Fixes the following issues:

* Selection behavior
* Component selection is by header click only
* Newly created blocks within a unit should be selected on creation/save, appear selected, and have their sidebar open
* Some long text components seem to display at the default height rather than a longer height
* Within the full-page unit view, the "add to collection" overflow menu item on components does not seem to work/only opens the sidebar.
* Draft status indicator text is not vertically centered with icon
* When reordering, dragging a short component past a long component often causes a strange stutter effect.
* When dragging to reorder a component, moving quickly or scrolling often causes the drag handle to be lost / causes the block to jump somewhere else
* Reordering may not consistently support a keyboard-accessible option to change order, like in course authoring
* Tag button on component header opens the old tag side pane
2025-05-07 17:30:25 -05:00
Ihor Romaniuk
04e8f3a488 fix: manage access modal on duplicated xblock (#1866) 2025-05-07 15:40:09 -03:00
Rômulo Penido
d7173036a5 fix: unit pages ux bugs [FC-0083] (#1884)
This PR fixes some UX bugs related to the unit pages:

* Sort for "recently modified" on unit tab does not update after adding new components to units
* Change component delete warning message
2025-05-06 20:34:07 -05:00
edX requirements bot
208b0c9195 chore: update browserslist DB (#1907)
Co-authored-by: abdullahwaheed <42172960+abdullahwaheed@users.noreply.github.com>
2025-05-05 00:21:37 +00:00
Jillian
24e469542d perf: use Library search results to populate container card preview (#1820)
* fix: use Library search results to populate container card preview

* feat: show published children when showing only published Unit content

* fix: nits
2025-05-01 19:13:43 -07:00
Rômulo Penido
0fdc460c5b fix: several library unit page UX bugs (#1868)
* fix: rename "Organize" tab to "Manage"

* fix: duplicate key warnings

* fix: uniform messages while adding to collection

* fix: do not allow units be added to a unit
2025-05-01 15:31:43 -07:00
renovate[bot]
a7b10495e6 fix(deps): update dependency react-textarea-autosize to v8.5.9 (#1769)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-01 15:34:49 -04:00
renovate[bot]
fdfc30dbd5 fix(deps): update dependency react-select to v5.10.1 (#1787)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-01 15:20:12 -04:00
dependabot[bot]
75e0531c5b chore(deps): bump @babel/helpers from 7.25.0 to 7.27.0 (#1763)
Bumps [@babel/helpers](https://github.com/babel/babel/tree/HEAD/packages/babel-helpers) from 7.25.0 to 7.27.0.
- [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.27.0/packages/babel-helpers)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-05-01 15:02:15 -04:00
dependabot[bot]
b697a44f36 chore(deps): bump http-proxy-middleware from 2.0.7 to 2.0.9 (#1840)
Bumps [http-proxy-middleware](https://github.com/chimurai/http-proxy-middleware) from 2.0.7 to 2.0.9.
- [Release notes](https://github.com/chimurai/http-proxy-middleware/releases)
- [Changelog](https://github.com/chimurai/http-proxy-middleware/blob/v2.0.9/CHANGELOG.md)
- [Commits](https://github.com/chimurai/http-proxy-middleware/compare/v2.0.7...v2.0.9)

---
updated-dependencies:
- dependency-name: http-proxy-middleware
  dependency-version: 2.0.9
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-05-01 14:52:19 -04:00
dependabot[bot]
db0f562d93 chore(deps): bump formidable from 3.5.2 to 3.5.4 (#1877)
Bumps [formidable](https://github.com/node-formidable/formidable) from 3.5.2 to 3.5.4.
- [Release notes](https://github.com/node-formidable/formidable/releases)
- [Changelog](https://github.com/node-formidable/formidable/blob/master/CHANGELOG.md)
- [Commits](https://github.com/node-formidable/formidable/commits)

---
updated-dependencies:
- dependency-name: formidable
  dependency-version: 3.5.4
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-05-01 14:42:16 -04:00
renovate[bot]
11835d28aa fix(deps): update dependency @edx/openedx-atlas to ^0.7.0 (#1855)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-01 13:55:41 -04:00
renovate[bot]
b023173ed4 fix(deps): update dependency @codemirror/view to v6.36.6 (#1315)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-01 13:40:13 -04:00
Navin Karkera
bc18fffedf refactor: remove custom order function from course libraries list (#1865) 2025-04-30 13:41:17 -07:00
Jansen Kantor
484154b9bd fix: pages and resources plugins not rendered (#1885) 2025-04-30 15:56:49 -04:00
Muhammad Faraz Maqsood
65aca04708 fix: Error when viewing text component in RAW HTML (#1869) 2025-04-28 16:51:54 -04:00
Ivo Branco
d92b27ee93 fix(i18n): translate files and videos table view (#1591)
Fix translation issue on Files and Videos table view mode on the columns.
2025-04-28 12:38:27 -05:00
Brayan Cerón
0f5c752eb0 fix: update MIME type for tar.gz file acceptance in dropzone (#1862)
This solves an issue that allowed the users to import files .gz.
We make the drop zone allow only .tar.gz files.

Resolves https://github.com/openedx/frontend-app-authoring/issues/1386
2025-04-28 12:45:36 -04:00
Raymond Zhou
3d2df5f4be chore: update readme on legacy flags (#1861) 2025-04-28 11:34:17 -04:00
Daniel Valenzuela
dbb1a996e1 feat: display editors as modals (#1838) 2025-04-25 19:48:28 +00:00
Jillian
b30a1c8c5e fix: NaN library components are out of sync [FC-0083] (#1864) 2025-04-25 10:08:33 -05:00
Chris Chávez
855b44f745 feat: Add 'This unit can only be edited from the library' banner (#1860) 2025-04-24 20:50:29 -07:00
Jillian
2d55ba4ccc fix: allow units sourced from libraries to update their settings (#1863) 2025-04-24 17:58:37 -07:00
Chris Chávez
d62c4cf4f8 feat: Sync units in course outline [FC-0083] (#1850)
* Adds the sync button in unit cards in the course outline.
* Opens the compare previews.
* Functionality to sync units.
* Functionality to decline sync units.
2025-04-24 20:18:04 +00:00
Rômulo Penido
9824502278 feat: disallow edits to units in courses that are sourced from a library (#1833) 2025-04-24 19:20:46 +00:00
Chris Chávez
d6b51ecf0c feat: Add unit from library into course (#1829)
* feat: Initial worflow to add unit to course
* test: Add initial tests
* feat: Show only published units
* test: Update Subsection card test and ComponentPicker tests
* feat: Connect add unit from library API
* test: Test for Add unit from library in CourseOutline
* fix: create a new Vertical from a Library Unit
* docs: add a little note about avoiding 'vertical' where possible
* refactor: Use visibleTabs instead of showOnlyHomeTab

---------

Co-authored-by: Jillian Vogel <jill@opencraft.com>
Co-authored-by: Braden MacDonald <braden@opencraft.com>
2025-04-24 12:07:54 -07:00
Muhammad Faraz Maqsood
1fe1f93314 fix: incorrect video durations (#1856)
- fix: incorrect video durations in video info tab and  videos list view

Co-authored-by: Muhammad Faraz  Maqsood <faraz.maqsood@A006-01130.local>
2025-04-24 13:17:40 -04:00
Brian Smith
fbc1273955 feat: import StudioFooterSlot from component package instead of slot package (#1832) 2025-04-24 12:58:27 -04:00
Brian Smith
7edb3528ba feat: standardize slot ids (#1854) 2025-04-24 07:27:04 -04:00
renovate[bot]
e7c22b1cbf fix(deps): update dependency @edx/frontend-component-header to v6.4.0 (#1790)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-04-23 16:24:47 -04:00
Muhammad Anas
380f3be164 feat: added markdown editor for editing problems in markdown format (#1805) 2025-04-23 13:24:27 -07:00
Chris Chávez
74d7d66c59 feat: New Transcript widget state on video editors on creation workflow [FC-0076] (#1825)
* New Transcript widget state on video editors on creation workflow
* Which edX user roles will this change impact? "Course Author",
2025-04-23 09:52:10 -05:00
Arunmozhi
e2189f2fdd feat: Enhance Sidebar Slot properties (#1845)
The commit add some extra properties to the CourseAuthoringSidebarSlot
and CourseAuthoringUnitSidebarSlot components to enable
the widgets in the sidebar to have more context to work with.
2025-04-23 09:46:00 -04:00
Muhammad Faraz Maqsood
293b7941dd fix: video & file page toggles
- react redux state changes back to default whenever page refreshes.
 - On course authoring mfe, whenever we redirect from one page to another, it automatically refreshes the page which react app shouldn't do.
 - So, instead of managing video and file pages previously selected view in react redux, save & manage these values in localStorage. So that page refreshes doesn't bother end users.
2025-04-23 12:22:17 +05:00
Rômulo Penido
eaa075464c feat: rename component on library unit page (#1823) 2025-04-22 22:16:38 -07:00
Muhammad Faraz Maqsood
03d732846e fix: TypeError r is not function for custom pages
- fix TypeError r is not a function for custom pages by replacing injectIntl with useIntl() hook.
- fix import for textbooks app
2025-04-23 10:13:45 +05:00
Muhammad Faraz Maqsood
c1302f1089 fix: TypeError r is not a function
- injectIntl is deprecated, Used useIntl() hook instead.
- ErrorLog
```
React Router caught the following error during render TypeError: r is not a function {
  "componentStack": "
    at A (https://course-authoring.edx.org/817.bfc0047cf532fb354633.js:2:5259944)
    at Suspense
    at d (https://course-authoring.edx.org/817.bfc0047cf532fb354633.js:2:65420)
    at t (https://course-authoring.edx.org/817.bfc0047cf532fb354633.js:2:7316739)
    at g (https://course-authoring.edx.org/817.bfc0047cf532fb354633.js:2:8043115)
    at w (https://course-authoring.edx.org/817.bfc0047cf532fb354633.js:2:4746781)
    at div
    at https://course-authoring.edx.org/817.bfc0047cf532fb354633.js:2:6453516
    at div
    at https://course-authoring.edx.org/817.bfc0047cf532fb354633.js:2:7617877
    at div
    at h (https://course-authoring.edx.org/817.bfc0047cf532fb354633.js:2:4740935)
    at rR (https://course-authoring.edx.org/app.756074826164c8adbdbb.js:2:1102691)
    at r (https://course-authoring.edx.org/817.bfc0047cf532fb354633.js:2:1493518)
    at injectIntl(r)
    at main
    at l (https://course-authoring.edx.org/app.756074826164c8adbdbb.js:2:2224276)
    at cR (https://course-authoring.edx.org/app.756074826164c8adbdbb.js:2:1103620)
    at r (https://course-authoring.edx.org/817.bfc0047cf532fb354633.js:2:1493518)
    at injectIntl(r)"
}
```
2025-04-23 10:13:45 +05:00
Chris Chávez
ea26981393 feat: Enable ORA2 in libraries by default (#1847) 2025-04-22 14:34:34 -07:00
Braden MacDonald
55e505eb36 chore: simplify imports in library-authoring/data/apiHooks.ts (#1842)
This just simplifies how API methods are imported into one apiHooks.ts file, reducing the overall lines of code and chance for conflicts. Since we're importing all the API methods anyways, there is nothing to gain from explicitly importing each one separately.
2025-04-22 11:50:06 -05:00
Muhammad Faraz Maqsood
9002f7acfe chore: upgrade footer version 2025-04-22 12:28:18 +05:00
edX requirements bot
febf5cf5d0 chore: update browserslist DB (#1839)
Co-authored-by: abdullahwaheed <42172960+abdullahwaheed@users.noreply.github.com>
2025-04-21 00:20:48 +00:00
Raymond Zhou
ac127e2b15 Revert "fix: use navigate instead of Link from react-dom"
This reverts commit 06bdff1796.
2025-04-19 10:26:42 +05:00
Muhammad Faraz Maqsood
06bdff1796 fix: use navigate instead of Link from react-dom
getting TypeError: r is not a function. Replace Link with navigate.
2025-04-18 21:26:22 +05:00
Braden MacDonald
ea0a031d7b feat: button to publish a container [FC-0083] (#1827)
- Publish button with functionality of publish units and components inside the unit
2025-04-18 09:34:46 -05:00
Muhammad Faraz Maqsood
ea8a8e5285 fix: toggle behaviour for video & file view
- fix toggle behaviour for video and file view.
- Before:
  - The default view was card. And The videos and files both pages were sharing same variable & default view.
  - Whenever user selects list view on videos/files page and redirects to another page, the toggle/view shifts again to default(card) view whenever it returns to videos/files page.

- After:
  - The default view is list now. And The videos and files both pages can have different state & default view.
  - Whenever user selects card view on videos/files page and redirects to another page, the toggle/view remain same whatever user had selected before when it returns to videos/files page.

Note: Refreshing a page will use default(list) view.
2025-04-18 11:13:32 +05:00
Chris Chávez
9adfa58d65 feat: Remove component from unit [FC-0083] (#1824)
* Users can remove a component from a unit
* The component is NOT deleted, and remains present in the library
* A toast shows that the component was removed, and allows the user to undo
* Overflow menu item appears in sidebar for selected components in unit
* Overflow menu item appears directly on components in full page unit view
2025-04-17 17:51:42 -05:00
Navin Karkera
4ddb8c3168 feat: edit components in unit page [FC-0083] (#1821)
Allows authors to edit components from unit page. It makes sure that the component preview is updated on save, allows user to double click and open editor in modal etc.
2025-04-17 09:59:16 -05:00
Navin Karkera
3b2adc2fc1 feat: reorder components in unit page [FC-00083] (#1816)
Reorders components in unit page via drag and drop. This PR also refactors and moves draggable list and sortable item components to appropriate location.

Course authors will be affected by this change.
2025-04-16 14:34:28 -05:00
Régis Behmo
4bd2c3b29a feat: lighter build by rewriting lodash imports (#1772)
Incorrect lodash imports are causing MFEs to import the entire lodash
library. This change shaves off a few kB of the compressed build.
2025-04-15 17:07:16 -07:00
Braden MacDonald
f531d5471d fix: merge errors in previous commit (#1819) 2025-04-15 23:46:16 +00:00
Braden MacDonald
f24b89c847 feat: allow pasting units from a course into a library (#1812) 2025-04-15 15:26:19 -07:00
Rômulo Penido
d9dcdfe1e3 feat: add existing components to unit [FC-0083] (#1811)
allows adding existing components to units
2025-04-15 16:49:53 -05:00
Rômulo Penido
990073cb38 feat: renames unit in LibraryUnitPage and adds InplaceTextEditor component (#1810) 2025-04-15 15:42:36 -05:00
Jillian
afecd8ba83 fix: sort Advanced Blocks by default display name (#1817) 2025-04-15 15:07:21 -05:00
Rômulo Penido
aa8a5bfba4 feat: add collections support for containers [FC-0083] (#1797)
Adds support to add Units to Collections.
2025-04-15 13:13:12 -05:00
Navin Karkera
87695ae636 fix: auto adjust min height of xblock previews [FC-0083] (#1813)
Sets minimum height of library block previews based on its render
location and block type.
2025-04-15 10:37:59 -05:00
Braden MacDonald
681854209a fix: Copy to clipboard would seemingly fail even if it worked 2025-04-14 17:21:10 -07:00
Chris Chávez
a522c48045 feat: Add component to Unit [FC-0083] (#1784)
Creation workflow in unit page.
2025-04-14 22:36:46 +00:00
edX requirements bot
f46e4ce4e8 chore: update browserslist DB (#1814)
Co-authored-by: abdullahwaheed <42172960+abdullahwaheed@users.noreply.github.com>
2025-04-14 00:21:14 +00:00
Navin Karkera
a43027b328 feat: library unit page skeleton [FC-0083] (#1779)
* View a unit page, which has its own URL
* Components appear within a unit as full previews. Their top bar shows type icon and title on the left, and draft status (if any), tag count, overflow menu, and drag handle on the right.
* Components have an overflow menu within a unit
* Components can be selected within a unit
* When components are selected, the standard component sidebar appears. The preview tab is hidden, since component previews are visible in the main content area.
* Components within a unit full-page view have hover and selected states
* Unit sidebar preview.
* Frontend implementation Drag-n-drop components to reorder them in unit.
2025-04-11 13:50:40 -05:00
Muhammad Anas
01365d080e feat: replace StudioFooter with StudioFooterSlot (#1729) 2025-04-11 13:29:41 -04:00
Chris Chávez
e00a4c4d03 refactor: Build new action buttons for cancel confirmation modal (#1732)
Build new action buttons for cancel confirmation modal in basic block and advanced block editors.
2025-04-11 11:55:32 -05:00
Kshitij Sobti
341a03c50b feat: Add plugin slots for sidebars (#1752) 2025-04-11 07:19:13 -04:00
Chris Chávez
5df7adffec feat: Delete unit [FC-0083] (#1773)
* Adds the delete menu item in unit cards.
* Delete a unit with a confirmation modal.
* Restore a component
2025-04-10 23:59:10 +00:00
Rômulo Penido
04faf54ad8 feat: add container (e.g. unit) tag support (#1782) 2025-04-09 12:10:31 -07:00
Bryann Valderrama
d688cf57b7 fix: add user partition id when update groups 2025-04-09 11:55:11 -07:00
sundasnoreen12
fe36e65d2d fix: removed all varaibles of feedback url 2025-04-09 12:45:56 +05:00
sundasnoreen12
8e99ebf072 test: removed variables from test file 2025-04-09 12:45:56 +05:00
sundasnoreen12
024537c80e feat: hide feedback widget 2025-04-09 12:45:56 +05:00
Muhammad Faraz Maqsood
0ddcbbb7a5 fix: ignore altText to avoid replace &quot; with "
- ignore altText to avoid replacing &quot; with "(double quotes) in alt text value
- modify unit tests to cover this new code
2025-04-09 11:58:22 +05:00
Jillian
7ceeb32820 feat: Unit card previews [FC-0083] (#1774)
Adds block tiles to the Unit card to indicate type and quantity of children in the container.
2025-04-08 17:47:04 -05:00
Sarina Canelake
451b821c3b docs: Update references to docs.edx.org (#1783)
With the transition to docs.openedx.org, update links to docs.edx.org to the community-supported site.
2025-04-07 13:55:19 -04:00
Rômulo Penido
68d62cd62f feat: library unit sidebar [FC-0083] (#1762)
Implements the placeholder for the Unit Sidebar.
2025-04-07 11:51:10 -05:00
Rômulo Penido
2a31434a55 fix: prevent multiple submits while creating units [FC-0083] (#1776)
Fixes a bug where the form is submitted multiple times when the user presses Enter on the Unit create form.

The issue was that when the user hit Enter, the form was submitted without calling the button's onClick, allowing multiple calls. Also, because the onClick was not called, we had to add the isLoading property to the LoadingButton to display the status correctly.
2025-04-07 09:24:51 -05:00
Muhammad Faraz Maqsood
fdd8928f36 feat: add manual check count to collapsed view 2025-04-07 10:40:11 +05:00
Muhammad Faraz Maqsood
552ff395df chore: rename course optimizer name in dropdown 2025-04-07 10:40:01 +05:00
edX requirements bot
c324446722 chore: update browserslist DB (#1785)
Co-authored-by: abdullahwaheed <42172960+abdullahwaheed@users.noreply.github.com>
2025-04-07 00:20:14 +00:00
Arunmozhi
15fcb55075 feat: adds slots for in-context metrics in studio outline and unit views (#1725) 2025-04-04 07:29:36 -04:00
Régis Behmo
d1a6af51a4 chore: remove husky 🪓🐶
We remove husky, which is triggering pre-push git hooks, including
running "npm lint". This is causing failures when building Docker
images, because "npm clean-install --omit=dev" automatically triggers "npm
prepare", which attemps to run "husky". But husky is not listed in the
build dependencies, only in devDependencies. As a consequence, package
installation is failing with the following error:

        14.13 > @edx/frontend-app-ora-grading@0.0.1 prepare
        14.13 > husky install
        14.13
        14.15 sh: 1: husky: not found

Similar to: https://github.com/openedx/frontend-app-learning/pull/1622
2025-04-02 14:00:02 -07:00
sarina
55344bc55d docs: Update edx.rtd links to docs.openedx.org 2025-04-02 15:59:31 -04:00
sarina
a23f6a6fa7 docs: Update README: s/devstack/tutor/ 2025-04-02 15:59:31 -04:00
sarina
5cedaacc3e docs: Add note to use .nvmrc node version 2025-04-02 15:59:31 -04:00
Brian Smith
0ce0b7526e feat: upgrade to react 18 (#1766) 2025-04-02 12:04:48 -04:00
Ihor Romaniuk
3685dbd6a1 feat: [FC-0070] rendering split test content in unit page (#1492)
Introduces functionality to display Split Test Content within the new Unit page interface.
2025-04-01 11:37:14 -03:00
Rômulo Penido
272e30f1b1 feat: library units tab (#1754)
Implements the "Units" tab on the Library Authoring.
2025-03-31 13:34:25 -05:00
Rômulo Penido
98ae74e78c feat: unit cards in library [FC-0083] (#1742)
Unit cards in library and rename BaseComponentCard -> BaseCard
2025-03-31 10:54:07 -05:00
Rômulo Penido
df7405ec39 feat: create unit workflow (#1741)
Implements the basic workflow to create a Unit in a Library.
2025-03-28 15:44:58 -05:00
Muhammad Farhan
d497bf2ccc fix: update useRef before dispatching file upload action 2025-03-28 19:38:06 +05:00
Muhammad Faraz Maqsood
94f34074ce fix: link navigation for BrokenLink and GoToBlock
In this commit, Fix link navigation in BrokenLinkHref and GoToBlock components
- Updated BrokenLinkHref to prevent default anchor behavior and open broken links directly in a new tab.
- Updated GoToBlock to prevent default anchor behavior and open block URLs directly in a new tab.
- added test coverage for this fix code in `CourseOptimizerPage.test.js`, this wasn't covered before: https://github.com/openedx/frontend-app-authoring/pull/1760/checks?check_run_id=39390124321
2025-03-28 12:19:29 +05:00
Brian Smith
92a8b42e36 feat(deps): update @openedx dependencies to versions that support React 18 (#1759) 2025-03-27 12:19:00 -04:00
Muhammad Farhan
08368582e3 fix: video ID not populating issue 2025-03-26 23:07:27 +05:00
Navin Karkera
a52f6d9b94 fix: prevent editor from loading twice before initialization (#1761)
Data from previous editor instance was being processed by current editor
instance and sometimes failed due to mismatch. For example, editing text
editor or any other basic editor after opening an advanced problem like
drag-n-drop crashed. Now the editor is only rendered after the
initialization process is complete.
2025-03-25 20:14:57 -05:00
Navin Karkera
bac63583ac refactor: don't dismiss out of sync alert on review (#1750)
Clicking review button on out of sync alert should not dismiss the alert
and it should be displayed again in outline or all tab.
2025-03-24 11:39:10 -05:00
Hina Khadim
545bb4a8a6 feat: add manual check links section separately for 403 links (#1751) 2025-03-24 19:55:56 +05:00
Kyle McCormick
9e65424ca6 refactor: Remove unused defaultToAdvanced and getBetaParsedOLXData (#1753)
edx-platform would pass a default_to_advanced flag in through the REST
API, depending on the value of a waffle flag. The flag did not actually
cause anything to default to advanced. What it actually did was switch
from getParsedOLXData to getParsedBetaOLXData. However, getBetaOLXParser
was never implemented--it just logs a console warning and return
getOLXParser.

We remove this unused flag and unused function.
The underlying default_to_advanced API flag and the backing waffle flag
will be removed from edx-platform in a separate PR.
2025-03-20 15:49:36 -04:00
Navin Karkera
27c4eec746 refactor: open review tab in course libraries if out of sync (#1743) 2025-03-19 12:49:22 -05:00
Muhammad Farhan
cc20dfd8ca fix: fetch only studio home data without courses 2025-03-19 12:57:03 +05:00
Rômulo Penido
a26e3f9e92 fix: show error information when taxonomy import fails (#1730)
Adds the error information when we have a failure while importing a Taxonomy
2025-03-17 10:00:33 -05:00
Navin Karkera
e66da2cb49 fix: image rendering in single and multi select problems (#1731)
Fix images in single and multi select problems in libraries. Found following issues and fixed them:

* Images were not being rendered in any of the fields in these problems.
* Base url was not being set which is used by tinymce to load images with relative path.
* Answer fields were set to inline mode which does not initialize images or base paths
* If same image twice is used twice in a problem, the logic of replacing `static/image.jpg` with `/static/image.jpg` would replace the first occurrence twice resulting in `//static/image.jpg`, breaking both the links.
* On initialization of answer fields, the absolute static asset urls were being replaced by relative urls causing the editor being set as dirty without user changes.
2025-03-13 11:11:44 -05:00
Navin Karkera
77a55d9ad3 feat: course libraries review tab [FC-0076] (#1699)
Adds review tab to course libraries page. Also refactors all libraries page as per new designs.
2025-03-12 12:58:27 -05:00
Chris Chávez
3aa409d065 feat: Add Publish confirmation modal [FC-0076] (#1677) 2025-03-11 17:37:40 -05:00
Muhammad Farhan
732fd28eb9 refactor: Improve conditions readability 2025-03-12 00:08:06 +05:00
Muhammad Farhan
091e120224 fix: Use defaultValue when item is null or empty 2025-03-12 00:08:06 +05:00
Brayan Ceron
1174b09ac4 fix: render proper visibility message on self-paced course type 2025-03-11 16:50:59 +05:00
Chris Chávez
b2472cfc0a refactor: Separate Publish order sort from publish filter [FC-0076] (#1701) 2025-03-10 10:55:15 -05:00
Rômulo Penido
17ebb90cd1 fix: paste button on unit page wasn't working (#1724) 2025-03-07 11:09:00 -08:00
Hina Khadim
49fbe766b0 fix: Prevent Alt Text from Being Truncated Due to Double Quotation Marks (#1721) 2025-03-07 23:17:37 +05:00
Rômulo Penido
dbba4dd296 fix: excessive calls to the clipboard API endpoint (#1700) 2025-03-07 08:16:39 -08:00
Chris Chávez
0eda5aec23 feat: Create advanced blocks in libraries [FC-0076] (#1653)
List view to show and create the advanced blocks
2025-03-05 12:46:17 -05:00
Dima Alipov
26c919a070 fix: default setting not updating with updated default settings 2025-03-04 17:01:39 +05:00
Demid
e9d85e85d3 fix: broken re-run notification dismiss (#1590) 2025-03-04 16:59:31 +05:00
Dmytro
e100193744 fix: prevent passing empty string in the date field to backend (#1457)
Co-authored-by: Dima Alipov <dimaalipov@192.168.1.101>
2025-03-04 16:59:00 +05:00
Peter Kulko
411607ec59 feat: [FC-0070] Manage Tags interoperation (#1454)
Added interaction between MFE and Legacy tagging functionality for xblocks on the Course unit page.
2025-03-03 11:07:04 -03:00
Rômulo Penido
06d591df13 fix: change ComponentPreview modal size to xl (#1650) 2025-02-26 17:27:51 -05:00
Jillian
e5360dc1f1 fix: allow users who can create orgs to see the full list of orgs when creating a course [FC-0076] (#1689)
The fixes for https://github.com/openedx/frontend-app-authoring/issues/1577 caused issues with edx.org's Global Staff users not being able to create courses. edx.org/2U Global Staff are seeing an empty Organization drop-down list.

This issue arose due to misuse of two similarly-named flags returned by the contentstore's home API

* `can_create_organizations`: granted to Global Staff or everyone if `FEATURES[ENABLE_CREATOR_GROUP] == True`, 
* `allow_to_create_new_org`: which is actually about auto-creating organizations, so both `can_create_organizations` + `settings.ORGANIZATIONS_AUTOCREATE` must be True

In this change, we use `canCreateOrganizations` to decide whether the user can see the full list of organizations when creating courses. We preserve the use of `allowToCreateNewOrg`  when deciding whether to allow users to "typeahead" to create a new organization not in the drop-down list.
2025-02-25 21:07:43 -05:00
Navin Karkera
11a7e78b73 fix: parsing text input problems (#1679)
* Parse incorrect answer fields in text input problems
* Fix feedback order
2025-02-25 15:41:49 -05:00
Rômulo Penido
56b7a7b17a feat: add component usage data in the ComponentDetails component [FC-0076] (#1656)
Adds the list of Courses and Units/Containers using a component to the "Details" tab on the sidebar.
2025-02-25 13:55:36 -05:00
Diana Olarte
6b2ba6e063 feat: cancellable block creation/edit workflow in libraries v2 (#1574)
Before, clicking "new problem" (etc) would create a new block, then launch the editor. Now it launches the editor and then creates the new block only on save. This makes the "Cancel" button work as expected. Only affects libraries so far, not courses.
2025-02-25 10:28:42 -08:00
Hina Khadim
63caf098a5 [TNL-11885] fix: resolve course-optimizer failure case bug (#1674)
* fix: resolve course-optimizer failure case bug

---------

Co-authored-by: Hina Khadim <hina.khadim@PF1H334R.2tor.net>
2025-02-25 14:52:16 +05:00
Kyle McCormick
8fe52d22e7 fix: Upgrade header, temporarily re-introducing Maintenance link (#1676)
We are temporarily re-introducing the Maintenance link, as the
Maintenance Announcements tool is still in use, as discussed on:
openedx/edx-platform#35852

There are no other significant changes in this version bump:
https://github.com/openedx/frontend-component-header/compare/v5.8.0...v5.8.3
2025-02-24 09:41:06 -05:00
Muhammad Faraz Maqsood
a0a0b9dc84 fix: Resolve Course Optimizer Scanning Bug
- Corrected LINK_CHECK_STATUSES.IN_PROGRESS value that is sent by backend.
2025-02-24 13:31:28 +05:00
Chris Chávez
ba896a3b15 feat: Enable transcripts for video library [FC-0076] (#1596)
* Get updateTranscriptHandlerUrl() and call it when is ready.
* Enable LanguageNamesWidget in a library.
* Enable add transcripts for libraries.
* Enable delete transcripts for libraries.
* Enable replace transcripts for libraries.
* Enable download transcripts for libraries.
* Enable download transcripts from YouTube
2025-02-21 13:33:50 -05:00
KEVYN SUAREZ
3e235d3360 fix: Adjust course import dropzone height to make course import status visible (#1664)
Closes: https://github.com/openedx/frontend-app-authoring/issues/1387
2025-02-21 16:48:57 +00:00
Chris Chávez
0db1727537 feat: Add cancel confirmation modal to Advanced editors in libraries [FC-0076] (#1672)
* Extracts the cancel confirmation modal as a new component.
* Adds the cancel confirmation modal to the Advanced editors in libraries.
2025-02-21 11:24:23 -05:00
Peter Kulko
7e4ecff4e8 feat: handle edit modals from advanced xblocks (#1445)
Adds new message types, updates message handlers, and implements a new modal iframe for legacy XBlock editing.
2025-02-20 17:05:48 -03:00
Saad Yousaf
2befd82e51 refactor: Update information message on Course Optimizer page 2025-02-20 16:57:17 +05:00
Navin Karkera
8275bbe8ce feat: course libraries page [FC-0076] (#1641)
Adds Libraries page that lists all library components being used in the current course to Content > Libraries
2025-02-18 10:36:31 -05:00
Chris Chávez
59243b0cb3 fix: Height in Advanced Editor [FC-0076] (#1649)
Updates the height of the Advanced Editor.
2025-02-13 11:22:02 -05:00
Braden MacDonald
0b08d82f03 fix: bugs with ExpandableTextArea toolbars & modals in problem editor (#1646)
* fix: clicking library name in Studio header would show 404

* fix: when ExpandableTextArea is in a modal, the selection toolbar could not be clicked

* fix: in ExpandableTextArea, shrink the "insert toolbar" that blocks the input

* chore: ignore coverage of modal fixer

* fix: make sure emoji/formula modals are working in the text editor too
2025-02-12 13:40:18 -08:00
Peter Kulko
e9130d3852 chore: iframe rendering optimization (#1544)
Iframe reload optimizations for various xblock related actions. Added some improvements related to scrolling to the current xblock. Fixed behavior of the xblock action dropdown list.
2025-02-11 16:31:07 -03:00
Chris Chávez
b0fc3d923b feat: Add drag and drop to LIBRARY_SUPPORTED_BLOCKS (#1651)
Add drag and drop v2 to LIBRARY_SUPPORTED_BLOCKS to enable the block by default
2025-02-07 12:47:33 -05:00
Daniel Valenzuela
05dddce920 feat: allow filtering library by publish status (#1570)
* Adds a filter widget for Publish status.
* Adds "Unpublished changes" badge.
2025-02-05 13:09:27 -05:00
Muhammad Farhan
31f39cb015 fix: regex mismatch in text block (#1597) 2025-01-30 09:13:26 -05:00
Braden MacDonald
b7241a124c docs: clarify/update a TODO comment about Meilisearch functionality (#1638)
Just a small update to a comment in the code, now that Meilisearch has new functionality that implements a feature we need.
2025-01-29 12:20:48 -05:00
Jillian
be600a91f0 feat: hide some settings fields when editing a library problem (#1601)
Hides some XBlock settings fields when editing library blocks. These hidden settings fields are relevant to course blocks, but not library blocks.

This change impacts Library Authors, and Course Authors who use Library Blocks and/or Problem Banks.
2025-01-27 10:47:09 -05:00
salman2013
de7affd97f chore: update catalog-info file and remove openedx.yaml file 2025-01-24 09:35:54 -05:00
Kristin Aoki
2102c7a612 fix: hidden remove button (#1600)
This PR fixes the visibility of the "Remove" button for grade segments. Previously, the button would appear on hover above the segment. But the styling of the button blocked it from being visible. Now, the button appears on hover under the grade range. This change impacts Course Authors.
2025-01-24 17:25:15 +05:00
Jillian
654c06b596 fix: a11y and performance issues with library authoring navigation [FC-0076] (#1593)
Fixes some a11y and performance issues for Library Authors

**Accessibility**

* When navigating between the various Library Authoring pages, the search keyword box should not be auto-focused, so focus will follow the user's click/tab events and keep that continuity.
The course content search modal behavior is unchanged -- loading this modal auto-focuses the search input box as one would expect.

**Performance**

* Navigating between the various Library Authoring pages was causing a full re-mount of the `<LibraryLayout/>` component
* React query's `staleTime` option is used to control how long data is considered fresh in both queries and routes. By default, the `staleTime` for queries is set to 0 milliseconds, meaning that the data will always be considered stale and will be refetched whenever the query is mounted.
2025-01-22 11:39:47 -05:00
Rômulo Penido
13b2ed5363 feat: add last published date to HistoryWidget [FC-0076] (#1585)
Adds the "Last Published" date to the `HistoryWidget`
2025-01-20 12:30:10 -05:00
Chris Chávez
fd6a6dd443 feat: Add AdvancedEditors with an iframe [FC-0076] (#1568)
Creates the AdvancedEditor to support editors like Drag and Drop, openresponse, poll, survey, and other advanced editors.

- AdvancedEditor created to call studio_view of the block
- Update LibraryBlock to support any view (and use studio_view in AdvancedEditor)
Intercept xblock-event message to close the Advanced editor on cancel or save
2025-01-16 13:22:27 -05:00
Ihor Romaniuk
619ab9a267 feat: [FC-0070] rendering library content in unit page (#1475)
The enables opening a Library Content page within the new Studio unit page. This page displays the xBlocks from the specified library and provides basic configuration options for the library.
2025-01-16 14:06:48 -03:00
Peter Kulko
98fbcff842 feat: [FC-0070] listen to xblock interaction events (#1431)
This is part of the effort to support the new embedded Studio Unit Page.  It includes changes to the CourseUnit component and the functionality of interaction between xblocks in the iframe and the react page.

The following events have been processed:

* delete event
* Manage Access event (opening and closing a modal window)
* edit event for new xblock React editors
* clipboard events
* duplicate event
2025-01-15 13:28:43 -05:00
Jillian
45f6ef42a7 fix: allow user provided value if can auto-create orgs [FC-0076] (#1582)
Allows Content Authors to auto-create an organization when creating a library, if auto-creating orgs is allowed by the platform.
2025-01-15 12:44:30 -05:00
Jesper Hodge
8385c4e8ed Feat course optimizer page (#1533)
Course Optimizer is a feature approved by the Openedx community that adds a "Course Optimizer" page to studio where users can run a scan of a course for broken links - links that point to pages that have a 404.

Depends on backend: openedx/edx-platform#35887 - test together.

This also requires adding a nav menu item to edx-platform legacy studio. That should be implemented before enabling the waffle flag on prod.

Links:
- [Internal JIRA ticket](https://2u-internal.atlassian.net/browse/TNL-11809)
- [Course Optimizer Discovery](https://2u-internal.atlassian.net/wiki/spaces/TNL/pages/1426587703/TNL-11744+Course+Optimizer+Discovery)
- [Openedx community proposal](https://github.com/openedx/platform-roadmap/issues/388)
2025-01-13 11:44:25 -05:00
Farhaan Bukhsh
e6bce560bc feat: Adding human readable 403 error access restricted (#1569)
Updated to have human-readable forbidden error (403)
2025-01-10 13:22:49 -05:00
Jillian
811be226d1 feat: shareable URLs for library components and searches [FC-0076] (#1575)
Adds new routes and URL parameters to use when viewing and performing searches on library components. These changes allow these pages to be bookmarked or shared by copy/pasting the browser's current URL.

No changes were made to the UI.

Use cases covered:

* As an author working with content libraries, I want to easily share any component in a library with other people on my team, by copying the URL from my browser and sending it to them.
* As an author working with content libraries, I want to easily share any search results with other people on my team, by copying the URL from my browser and sending it to them.
* As an author working with content libraries, I want to bookmark a search in my browser and return to it at any time, with the same filters and keywords applied.
* As an author of a content library with public read access, I want to easily share any component in a library with any authors on the same Open edX instance, by copying the URL from my browser and sending it to them.
* As an author of a content library, I want to easily share a library's "Manage Team" page with other people on my team by copying the URL from my browser and sending it to them.
* As an author working with content libraries, I want to easily share any selected sidebar tab with other people on my team, by copying the URL from my browser and sending it to them.
2025-01-10 10:36:46 -05:00
edX requirements bot
f586b095fa chore: update browserslist DB (#1494)
Co-authored-by: abdullahwaheed <42172960+abdullahwaheed@users.noreply.github.com>
2025-01-06 10:38:02 -08:00
Rômulo Penido
dc0ba6aac4 fix: disable filter by block on collections tab [FC-0076] (#1576)
Adds the disabled prop for `FilterByBlockType` component and uses it on the Library Collection tab.
2024-12-21 07:01:22 -05:00
Rômulo Penido
230960b711 refactor: improve library sub header (#1573)
Changes the Library (and Collection) subheader
2024-12-18 12:56:21 -05:00
Chris Chávez
64906a1b9d refactor: Update error message while adding library team member [FC-0062] (#1572)
Update the error message while adding a library team member to show the error message generated by the server
2024-12-18 12:50:09 -05:00
Navin Karkera
b110b6bdc9 feat: undo component delete [FC-0076] (#1556)
Allows library authors to undo component deletion by displaying a toast message with an undo button for some duration after deletion.
2024-12-13 13:18:29 -05:00
Rômulo Penido
69bbeda816 refactor: split up library context (#1539)
Split the library context into smaller contexts:

* LibraryContext
* ComponentPickerContext
* SidebarContext
2024-12-12 13:14:45 -05:00
Brayan Cerón
c7e2bf9934 fix: find proper courses when searching (#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:44:02 -08:00
Daniel Valenzuela
73490a5741 fix: avoid changing url when removing filters (#1530)
* 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-06 16:24:04 -05:00
Chris Chávez
d2d753203f fix: Update error messages when adding user to library [FC-0062] (#1543)
Updates the message error when the user doesn't exist when adding a new team member to a library.
2024-12-06 15:46:52 -05:00
Rômulo Penido
0e9025a670 fix: legacy library links were not working (#1548) 2024-12-06 18:50:53 +00:00
Rômulo Penido
2f1263ab5a fix: show/hide "new library" button based on separate v1/v2 permissions (#1545) 2024-12-06 10:40:45 -08:00
Chris Chávez
a0f6f4357e fix: Show published OLX in Library Content Picker [FC-0062] (#1534) 2024-12-05 14:13:04 -05:00
Braden MacDonald
e75ce15a67 refactor: Add TypeScript types for the Editors' Redux state (#1537)
It starts to add type information to the messy Redux state used by the editors, mostly focusing on the state shared by all editors and the problem editor.
2024-12-03 17:51:52 -05:00
Rômulo Penido
0771923183 feat: preserve library sidebar tab while switching items (#1535)
In the library home page, makes the selected tab on the sidebar persist while selecting Components or Collections Info Sidebar.
2024-12-03 17:13:22 -05:00
Braden MacDonald
6e53e37bfe refactor: Convert more Taxonomy code to TypeScript (2) (#1536)
* Converts some files from .js or .mjs to .ts
* Refactors some tests to use the new initializeMocks helper
* Cleans up and improves some type definitions
2024-12-02 09:24:17 -05:00
Braden MacDonald
abe68ac599 refactor: Convert more Taxonomy code to TypeScript (#1532)
* Converts some files from .js or .mjs to .ts
* Moves the API code from src/taxonomy/tag-list/data into src/taxonomy/data
* Cleans up and improves some type definitions
* No user-visible changes / functionality changes.
2024-11-27 15:31:55 -05:00
Rômulo Penido
f86c609ff1 fix: editor flicker after creating xblock (#1531) 2024-11-26 14:41:04 -08:00
Kyle McCormick
ec3f78f0ea feat!: remove Maintenance header link (#1526)
...by bumping frontend-component-header 5.7.0 -> 5.8.0

Our reasoning is that the two functions of the Studio Maintenance
dashboard (Announcements and Maintenance Banner) have been broken
for a while.

It's actually version 5.7.2 that removes the link [1] but since 5.8.0
has no breaking changes, it seemed prudent to jump straight to latest.

[1] https://github.com/openedx/frontend-component-header/releases/v5.7.2

Related PR: https://github.com/openedx/edx-platform/pull/35852
2024-11-26 09:49:34 -05:00
Navin Karkera
55fe87a3db feat: show problem bank component picker on window msg [FC-0062] (#1522)
Fix for: If you have a unit with many components and a problem bank on the NEW MFE unit page (with an iframe), clicking "Add Components" will open a modal that's way too tall.
2024-11-22 20:29:18 +00:00
Navin Karkera
7aa5accdbb feat: preview library block changes in course unit [FC-0062] (#1506)
Creates a new preview library block modal. Intercepts the message when the block is iframed to open the new modal.
2024-11-22 15:18:43 -05:00
Diana Olarte
31f59d6bca fix: Schedule and Details page was not loading (#1527)
The page relied on obscure behavior of setting "isLibrary" to disable image uploads even in a course context. This commit refactors to use an explicit enableImageUpload prop in TinyMceWidget.
2024-11-22 12:10:11 -08:00
Peter Kulko
bc8d59b0eb chore: [FC-0070] Some tests refactoring (#1518) 2024-11-21 15:24:31 -03:00
Ihor Romaniuk
b5419acd74 feat: [FC-0070] implement move xblock modal (#1422) 2024-11-21 15:22:41 -03:00
Braden MacDonald
66577b0d59 chore: bump frontend-build, remove exceptions for prefer-default-export (#1519) 2024-11-20 09:00:02 -08:00
Chris Chávez
624f5addcf fix: Show published count component in library content picker (#1481)
When using the library component picker, show the correct number on component count (published components) in collection cards.
2024-11-19 16:10:00 -05:00
Chris Chávez
0365e3809b fix: TinyMce aux modal issues in text editors [FC-0062] (#1500)
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:10:04 -05:00
Diana Olarte
b260708080 test: fix contentContainer test (#1516) 2024-11-19 12:24:32 -05:00
Diana Olarte
f740f57454 feat: upload images to v2 library components from the TinyMCE in library editor (#1458) 2024-11-18 10:38:08 -08:00
Peter Kulko
ba48a273a1 fix: fixed Configure modal for unit page (#1452) 2024-11-18 11:39:44 -03:00
Peter Kulko
0706a09acb fix: error handling (#1079)
Added alert message if 403 error occurs.
2024-11-18 11:27:36 -03:00
Régis Behmo
771c5d3e19 docs: minor README improvements (#1504) 2024-11-15 10:32:22 -08:00
Kyle McCormick
6ffdb01c24 refactor: remove pointless maintenance link message (#1503)
This link is defined in frontend-component-header, so the message
shouldn't be here. Anyway, we are deleting the link from
frontend-component-header too

Related:
* https://github.com/openedx/frontend-component-header/pull/553
* https://github.com/openedx/edx-platform/pull/35852
2024-11-15 10:55:23 -05:00
Pooja Kulkarni
32e5fa68d8 fix: Adjust styling when title is truncated (#1382) 2024-11-14 17:30:19 -05:00
Navin Karkera
cee88885d9 feat: enable problem bank button functionality on unit page (#1480) 2024-11-14 12:00:23 -08:00
Navin Karkera
033acc45f1 fix: remove unnecessary toast notification on adding component (#1490)
Fix for: #1489
2024-11-14 12:03:09 -05:00
Navin Karkera
efd2b3d27d feat: show info banner in component picker (#1498)
Displays a infor banner if only published content is visible in component picker.
2024-11-13 13:54:20 -05:00
Peter Kulko
9b4cf8718f fix: fixing broken tests 2024-11-11 11:20:15 -03:00
Rômulo Penido
67faf9a63a fix: infinite scroll bug on library page (#1483) 2024-11-08 10:59:48 -08:00
Peter Kulko
e59f2846e3 feat: render iframe with xblocks (#1375)
This refactors the CourseUnit component by removing the DraggableList and CourseXBlock components and replacing them with a simpler XBlockContainerIframe. Additionally, it introduces new constants for iframe handling.
2024-11-08 10:53:32 -03:00
Peter Kulko
f9ef00e29f feat: [FC-0070] Remove backend redirects (use SPA functionality) (#1372)
Introduces the ability to utilize SPA functionality when the relevant waffle flags are enabled for current MFE pages. When any new MFE page is loaded, a request is made to retrieve the waffle flags. This includes both global waffle flags related to MFE Authoring pages, as well as waffle flags specific to the current course.
2024-11-08 08:19:23 -03:00
Daniel Valenzuela
979c69b48e feat: simplify Library Home Page (v2) (#1443) 2024-11-07 09:48:19 -08:00
Rômulo Penido
d99e3f0f62 fix: add spacing to searchbar and simplify render conditions (#1461)
Adds padding between the search bar and the library list.

Also, the render method was refactored to be a bit simpler.
2024-11-06 22:01:38 -05:00
Jillian
f1bdc6200f fix: show a more detailed error on Bad Request (#1468)
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.
2024-11-06 21:53:24 -05:00
Navin Karkera
e118eb5971 chore: hide transcripts in video preview for library (#1459)
Fixes: #1453
2024-11-05 15:15:28 -05:00
Jillian
d7bbd40de1 fix: Hide / error on Libraries v2 pages if !librariesV2Enabled (#1449)
Show an error message if the user tries to view a v2 Library while Libraries V2 are disabled in the platform.
2024-11-05 15:14:09 -05:00
edX requirements bot
fc94667a57 chore: update browserslist DB (#1381)
Co-authored-by: abdullahwaheed <42172960+abdullahwaheed@users.noreply.github.com>
2024-11-04 12:27:02 -08:00
Navin Karkera
df8a65dc4e feat: handle unsaved changes in text & problem editors (#1444)
The text & problem xblock editors will display a confirmation box before
cancelling only if user has changed something else it will directly go
back.
2024-11-04 12:41:00 -05:00
Rômulo Penido
949e4ac94c fix: enable publish button on library after component edit [FC-0062] (#1446)
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. Updates the apiHooks to invalidates library query.
2024-11-01 13:58:39 -05:00
Navin Karkera
549dbaa0fa fix: add component to collection on paste [FC-0062] (#1450)
Link component to collection if pasted in a collection page.
Fixes: https://github.com/openedx/frontend-app-authoring/issues/1435
2024-10-31 17:44:34 -05:00
Bilal Qamar
28569aa3da test: Remove support for Node 18 (#1247)
* test: Remove support for Node 18

* chore: update code coverage artifact naming
2024-10-31 15:32:54 -04:00
Rômulo Penido
ecfe27b043 fix: empty state for library selection on component picker [FC-0062] (#1440)
This PR fixes the empty state text for adding library content if the user can't access any library.
2024-10-28 18:49:18 -05:00
Chris Chávez
cff1177ae9 fix: Library Preview Expand button covers dropdown (#1438) 2024-10-25 13:55:24 -07:00
Kshitij Sobti
4d4adce715 feat: Use configured DEFAULT_GRADE_DESIGNATIONS (#1227)
Support was recently added to edx-platform to add customisised default grade
designations, this change adds support for them to this MFE as well to bring it
to partiy with the edx-platform UI

It also refactors the grading-settings page to use React Query and updates the
logic used when partitioning grades by default to make it work better when there
are more than 5 partitions.

Co-authored-by: Farhaan Bukhsh <farhaan@opencraft.com>
2024-10-25 15:34:04 +05:30
Jillian
774728a9c0 fix: use absolute URL for Export Tags menu item (#1432)
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
2024-10-24 21:03:51 -05:00
2032 changed files with 105620 additions and 51845 deletions

12
.env
View File

@@ -36,12 +36,18 @@ ENABLE_ASSETS_PAGE=false
ENABLE_VIDEO_UPLOAD_PAGE_LINK_IN_CONTENT_DROPDOWN=false
ENABLE_TAGGING_TAXONOMY_PAGES=true
ENABLE_CERTIFICATE_PAGE=true
ENABLE_COURSE_IMPORT_IN_LIBRARY=false
ENABLE_UNIT_PAGE_NEW_DESIGN=false
ENABLE_COURSE_OUTLINE_NEW_DESIGN=false
BBB_LEARN_MORE_URL=''
HOTJAR_APP_ID=''
HOTJAR_VERSION=6
HOTJAR_DEBUG=false
INVITE_STUDENTS_EMAIL_TO=''
ENABLE_HOME_PAGE_COURSE_API_V2=true
ENABLE_CHECKLIST_QUALITY=''
ENABLE_GRADING_METHOD_IN_PROBLEMS=false
LIBRARY_SUPPORTED_BLOCKS="problem,video,html"
# "Multi-level" blocks are unsupported in libraries
LIBRARY_UNSUPPORTED_BLOCKS="conditional,step-builder,problem-builder,library_content,itembank"
# Fallback in local style files
PARAGON_THEME_URLS={}
COURSE_TEAM_SUPPORT_EMAIL=''
ADMIN_CONSOLE_URL='http://localhost:2025/admin-console'

View File

@@ -37,6 +37,9 @@ ENABLE_UNIT_PAGE=false
ENABLE_ASSETS_PAGE=false
ENABLE_VIDEO_UPLOAD_PAGE_LINK_IN_CONTENT_DROPDOWN=true
ENABLE_CERTIFICATE_PAGE=true
ENABLE_COURSE_IMPORT_IN_LIBRARY=true
ENABLE_COURSE_OUTLINE_NEW_DESIGN=true
ENABLE_UNIT_PAGE_NEW_DESIGN=true
ENABLE_NEW_VIDEO_UPLOAD_PAGE=true
ENABLE_TAGGING_TAXONOMY_PAGES=true
BBB_LEARN_MORE_URL=''
@@ -44,7 +47,10 @@ HOTJAR_APP_ID=''
HOTJAR_VERSION=6
HOTJAR_DEBUG=true
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"
# "Multi-level" blocks are unsupported in libraries
LIBRARY_UNSUPPORTED_BLOCKS="conditional,step-builder,problem-builder,library_content,itembank"
# Fallback in local style files
PARAGON_THEME_URLS={}
COURSE_TEAM_SUPPORT_EMAIL=''
ADMIN_CONSOLE_URL='http://localhost:2025/admin-console'

View File

@@ -33,10 +33,14 @@ ENABLE_UNIT_PAGE=true
ENABLE_ASSETS_PAGE=false
ENABLE_VIDEO_UPLOAD_PAGE_LINK_IN_CONTENT_DROPDOWN=true
ENABLE_CERTIFICATE_PAGE=true
ENABLE_COURSE_IMPORT_IN_LIBRARY=true
ENABLE_COURSE_OUTLINE_NEW_DESIGN=false
ENABLE_UNIT_PAGE_NEW_DESIGN=false
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"
# "Multi-level" blocks are unsupported in libraries
LIBRARY_UNSUPPORTED_BLOCKS="conditional,step-builder,problem-builder,library_content,itembank"
PARAGON_THEME_URLS=
COURSE_TEAM_SUPPORT_EMAIL='support@example.com'

View File

@@ -14,6 +14,15 @@ module.exports = createConfig(
'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',
'no-restricted-imports': ['error', {
patterns: [
{
group: ['@edx/frontend-platform/i18n'],
importNames: ['injectIntl'],
message: "Use 'useIntl' hook instead of injectIntl.",
},
],
}],
},
settings: {
// Import URLs should be resolved using aliases

View File

@@ -2,26 +2,37 @@
Describe what this pull request changes, and why. Include implications for people using this change.
Design decisions and their rationales should be documented in the repo (docstring / ADR), per
[OEP-19](https://open-edx-proposals.readthedocs.io/en/latest/oep-0019-bp-developer-documentation.html), and can be linked here.
Useful information to include:
- Which edX user roles will this change impact? Common user roles are "Learner", "Course Author",
- Which user roles will this change impact? Common user roles are "Learner", "Course Author",
"Developer", and "Operator".
- Include screenshots for changes to the UI (ideally, both "before" and "after" screenshots, if applicable).
- Provide links to the description of corresponding configuration changes. Remember to correctly annotate these
changes.
## Supporting information
Link to other information about the change, such as Jira issues, GitHub issues, or Discourse discussions.
Link to other information about the change, such as GitHub issues, or Discourse discussions.
Be sure to check they are publicly readable, or if not, repeat the information here.
## Testing instructions
Please provide detailed step-by-step instructions for testing this change.
Please provide detailed step-by-step instructions for manually testing this change.
## Other information
Include anything else that will help reviewers and consumers understand the change.
- Does this change depend on other changes elsewhere?
- Any special concerns or limitations? For example: deprecations, migrations, security, or accessibility.
- Any special concerns or limitations? For example: deprecations, migrations, security, or accessibility.
## Best Practices Checklist
We're trying to move away from some deprecated patterns in this codebase. Please
check if your PR meets these recommendations before asking for a review:
- [ ] Any _new_ files are using TypeScript (`.ts`, `.tsx`).
- [ ] Avoid `propTypes` and `defaultProps` in any new or modified code.
- [ ] Tests should use the helpers in `src/testUtils.tsx` (specifically `initializeMocks`)
- [ ] Do not add new fields to the Redux state/store. Use React Context to share state among multiple components.
- [ ] Use React Query to load data from REST APIs. See any `apiHooks.ts` in this repo for examples.
- [ ] All new i18n messages in `messages.ts` files have a `description` for translators to use.
- [ ] Avoid using `../` in import paths. To import from parent folders, use `@src`, e.g. `import { initializeMocks } from '@src/testUtils';` instead of `from '../../../../testUtils'`

View File

@@ -0,0 +1,18 @@
# Run the workflow that adds new tickets that are labelled "release testing"
# to the org-wide BTR project board
name: Add release testing issues to the BTR project board
on:
issues:
types: [labeled]
# This workflow is triggered when an issue is labeled with 'release testing'.
# It adds the issue to the BTR project and applies the 'needs triage' label
# if it doesn't already have it.
jobs:
handle-release-testing:
uses: openedx/.github/.github/workflows/add-issue-to-btr-project.yml@master
secrets:
GITHUB_APP_ID: ${{ secrets.GRAPHQL_AUTH_APP_ID }}
GITHUB_APP_PRIVATE_KEY: ${{ secrets.GRAPHQL_AUTH_APP_PEM }}

15
.github/workflows/add-to-cc-board.yml vendored Normal file
View File

@@ -0,0 +1,15 @@
name: Trigger to add Issue or PR to a Core Contributor project board
on:
issues:
types: [labeled]
pull_request:
types: [labeled]
jobs:
add-to-cc-board:
if: github.event.label.name == 'Core Contributor assignee'
uses: openedx/.github/.github/workflows/add-to-cc-board.yml@master
with:
board_name: cc-frontend-apps
secrets:
projects_access_token: ${{ secrets.PROJECTS_TOKEN }}

View File

@@ -9,36 +9,43 @@ on:
jobs:
tests:
runs-on: ubuntu-latest
strategy:
matrix:
node: [18, 20]
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
- uses: actions/checkout@v6
- uses: actions/setup-node@v6
with:
node-version: ${{ matrix.node }}
node-version-file: '.nvmrc'
- run: make validate.ci
- name: Archive code coverage results
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v6
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
name: code-coverage-report
path: coverage/*.*
# We are trying out oxlint for a while. Please report if you ever see lint issues that eslint catches but oxlint
# misses. We expect the opposite (oxlint should catch more issues).
lint-preview:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: actions/setup-node@v6
with:
node-version-file: '.nvmrc'
- run: npm install
- run: npm run oxlint
coverage:
runs-on: ubuntu-latest
needs: tests
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
- name: Download code coverage results
uses: actions/download-artifact@v4
uses: actions/download-artifact@v7
with:
name: code-coverage-report-20
# When we're only using Node 20, replace the line above with the following:
# name: code-coverage-report
pattern: code-coverage-report
path: coverage
merge-multiple: true
- name: Upload coverage
uses: codecov/codecov-action@v4
uses: codecov/codecov-action@v5
with:
fail_ci_if_error: true
token: ${{ secrets.CODECOV_TOKEN }}

1
.gitignore vendored
View File

@@ -1,6 +1,7 @@
.DS_Store
.eslintcache
.idea
.run
node_modules
npm-debug.log
coverage

2
.nvmrc
View File

@@ -1 +1 @@
20
24

23
.oxlintrc.json Normal file
View File

@@ -0,0 +1,23 @@
{
"$schema": "./node_modules/oxlint/configuration_schema.json",
"categories": {
"correctness": "warn"
},
"rules": {
"eslint/no-unused-vars": "off",
"typescript/unbound-method": "off", // 🛑 TEMPORARY
"typescript/no-floating-promises": ["error", {
"allowForKnownSafeCalls": [
// queryClient.invalidateQueries returns a promise that can be awaited
// if you want to do something after all the subsequent refetches are
// complete, but we rarely if ever want that; we usually want to
// continue invalidating more things immediately. So we don't usually
// want to await this.
"invalidateQueries",
]
}]
},
"ignorePatterns": [
"webpack.dev-tutor.config.js",
]
}

View File

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

View File

@@ -38,9 +38,9 @@ Cloning and Setup
git clone https://github.com/openedx/frontend-app-authoring.git
2. Use node v20.x.
2. Use the version of Node specified in the ``.nvmrc`` file.
The current version of the micro-frontend build scripts supports node 20.
The current version of the micro-frontend build scripts supports the version of Node found in ``.nvmrc``.
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>`_.
@@ -85,8 +85,8 @@ 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:
``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
@@ -97,8 +97,8 @@ these commands to update your devstack's domain names:
* 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)
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
@@ -165,21 +165,7 @@ Feature: New React XBlock Editors
.. image:: ./docs/readme-images/feature-problem-editor.png
This allows an operator to enable the use of new React editors for the HTML, Video, and Problem XBlocks, all of which are provided here.
Requirements
------------
* ``edx-platform`` Waffle flags:
* ``new_core_editors.use_new_text_editor``: must be enabled for the new HTML Xblock editor to be used in Studio
* ``new_core_editors.use_new_video_editor``: must be enabled for the new Video Xblock editor to be used in Studio
* ``new_core_editors.use_new_problem_editor``: must be enabled for the new Problem Xblock editor to be used in Studio
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.
New React editors for the HTML, Video, and Problem XBlocks are provided here and are rendered by this MFE instead of by the XBlock's authoring view.
Feature: New Proctoring Exams View
==================================
@@ -189,14 +175,6 @@ Feature: New Proctoring Exams View
Requirements
------------
* ``edx-platform`` Django settings:
* ``ZENDESK_*``: necessary if automatic ZenDesk ticket creation is desired
* ``edx-platform`` Feature flags:
* ``ENABLE_EXAM_SETTINGS_HTML_VIEW``: this feature flag must be enabled for the link to the settings view to be shown
* `edx-exams <https://github.com/edx/edx-exams>`_: for this feature to work, the ``edx-exams`` IDA must be deployed and its API accessible by the browser
Configuration
@@ -214,23 +192,12 @@ In Studio, a new item ("Proctored Exam Settings") is added to "Other Course Sett
* Enable proctored exams for the course
* Allow opting out of proctored exams
* Select a proctoring provider
* Enable automatic creation of Zendesk tickets for "suspicious" proctored exam attempts
Feature: Advanced Settings
==========================
.. image:: ./docs/readme-images/feature-advanced-settings.png
Requirements
------------
* ``edx-platform`` Waffle flags:
* ``contentstore.new_studio_mfe.use_new_advanced_settings_page``: this feature flag must be enabled for the link to the settings view to be shown. It can be enabled on a per-course basis.
Feature Description
-------------------
In Studio, the "Advanced Settings" page for each enabled course will now be served by this frontend, instead of the UI built into edx-platform. The advanced settings page holds many different settings for the course, such as what features or XBlocks are enabled.
Feature: Files & Uploads
@@ -238,16 +205,6 @@ Feature: Files & Uploads
.. image:: ./docs/readme-images/feature-files-uploads.png
Requirements
------------
* ``edx-platform`` Waffle flags:
* ``contentstore.new_studio_mfe.use_new_files_uploads_page``: this feature flag must be enabled for the link to the Files & Uploads page to go to the MFE. It can be enabled on a per-course basis.
Feature Description
-------------------
In Studio, the "Files & Uploads" page for each enabled course will now be served by this frontend, instead of the UI built into edx-platform. This page allows managing static asset files like PDFs, images, etc. used for the course.
Feature: Course Updates
@@ -255,26 +212,11 @@ Feature: Course Updates
.. image:: ./docs/readme-images/feature-course-updates.png
Requirements
------------
* ``edx-platform`` Waffle flags:
* ``contentstore.new_studio_mfe.use_new_updates_page``: this feature flag must be enabled.
Feature: Import/Export Pages
============================
.. image:: ./docs/readme-images/feature-export.png
Requirements
------------
* ``edx-platform`` Waffle flags:
* ``contentstore.new_studio_mfe.use_new_export_page``: this feature flag will change the CMS to link to the new export page.
* ``contentstore.new_studio_mfe.use_new_import_page``: this feature flag will change the CMS to link to the new import page.
Feature: Tagging/Taxonomy Pages
================================
@@ -292,7 +234,7 @@ Configuration
In additional to the standard settings, the following local configuration items are required:
* ``ENABLE_TAGGING_TAXONOMY_PAGES``: must be enabled (which it is by default) in order to actually enable/show the new
* ``ENABLE_TAGGING_TAXONOMY_PAGES``: must be enabled (which it is by default) in order to actually enable/show the new
Tagging/Taxonomy functionality.
@@ -315,7 +257,7 @@ In additional to the standard settings, the following local configurations can b
Developing
**********
`Devstack <https://edx.readthedocs.io/projects/edx-installing-configuring-and-running/en/latest/installation/index.html>`_. If you start Devstack with ``make dev.up.studio`` that should give you everything you need as a companion to this frontend.
`Tutor <https://docs.tutor.edly.io/>`_ is the community-supported Open edX development environment. See the `tutor-mfe plugin README <https://github.com/overhangio/tutor-mfe?tab=readme-ov-file#mfe-development>`_ for more information.
If your devstack includes the default Demo course, you can visit the following URLs to see content:
@@ -326,7 +268,7 @@ Troubleshooting
========================
* ``npm ERR! gyp ERR! build error`` while running npm install on Macs with M1 processors: Probably due to a compatibility issue of node-canvas with M1.
Run ``brew install pkg-config pixman cairo pango libpng jpeg giflib librsvg`` before ``npm install`` to get the correct versions of the dependencies.
If there is still an error, look for "no package [...] found" in the error message and install missing package via brew.
(https://github.com/Automattic/node-canvas/issues/1733)
@@ -380,6 +322,20 @@ For more information about these options, see the `Getting Help`_ page.
.. _Getting Help: https://openedx.org/community/connect
Legacy Studio
*************
If you would like to use legacy studio for certain features, you can set the following waffle flags in ``edx-platform``:
* ``legacy_studio.text_editor``: loads the legacy HTML Xblock editor when editing a text block
* ``legacy_studio.video_editor``: loads the legacy Video editor when editing a video block
* ``legacy_studio.problem_editor``: loads the legacy Problem editor when editing a problem block
* ``legacy_studio.advanced_settings``: Advanced Settings page
* ``legacy_studio.updates``: Updates page
* ``legacy_studio.export``: Export page
* ``legacy_studio.import``: Import page
* ``legacy_studio.files_uploads``: Files page
* ``legacy_studio.exam_settings``: loads the legacy Exam Settings
License
*******

View File

@@ -12,7 +12,8 @@ metadata:
icon: "Web"
annotations:
openedx.org/arch-interest-groups: ""
openedx.org/release: "master"
spec:
owner: group:2u-tnl
owner: user:bradenmacdonald
type: 'website'
lifecycle: 'production'

View File

@@ -10,4 +10,6 @@ coverage:
threshold: 0%
ignore:
- "src/grading-settings/grading-scale/react-ranger.js"
- "src/generic/DraggableList/verticalSortableList.ts"
- "src/container-comparison/data/api.mock.ts"
- "src/index.js"

View File

@@ -0,0 +1,80 @@
Override External URLs
======================
What is getExternalLinkUrl?
---------------------------
The `getExternalLinkUrl` function is a utility from `@edx/frontend-platform` that allows for centralized management of external URLs. It enables the override of external links through configuration, making it possible to customize external references without modifying the source code directly.
URLs wrapped with getExternalLinkUrl
------------------------------------
Use cases:
1. **Accessibility Page** (`src/accessibility-page/AccessibilityPage.jsx`)
- `COMMUNITY_ACCESSIBILITY_LINK` - Points to community accessibility resources: https://www.edx.org/accessibility
2. **Course Outline** (if applicable)
- Documentation links
- Help resources
3. **Other pages** (search for `getExternalLinkUrl` usage across the codebase)
- Help documentation
- External tool integrations
Following external URLs are wrapped with `getExternalLinkUrl` in the authoring application:
- 'https://www.edx.org/accessibility'
- 'https://docs.openedx.org/en/latest/educators/concepts/exercise_tools/about_multi_select.html'
- 'https://docs.openedx.org/en/latest/educators/how-tos/course_development/exercise_tools/add_multi_select.html'
- 'https://docs.openedx.org/en/latest/educators/how-tos/course_development/exercise_tools/add_dropdown.html'
- 'https://docs.openedx.org/en/latest/educators/how-tos/course_development/exercise_tools/manage_numerical_input_problem.html'
- 'https://docs.openedx.org/en/latest/educators/how-tos/course_development/exercise_tools/add_text_input.html'
- 'https://docs.openedx.org/en/latest/educators/how-tos/course_development/social_sharing.html'
- 'https://docs.openedx.org/en/latest/educators/references/course_development/exercise_tools/guide_problem_types.html#advanced-problem-types'
- 'https://docs.openedx.org/en/latest/educators/references/course_development/parent_child_components.html'
- 'https://openai.com/api-data-privacy'
- 'https://docs.openedx.org/en/latest/educators/how-tos/course_development/create_new_library.html'
- 'https://bigbluebutton.org/privacy-policy/'
- 'https://creativecommons.org/about'
Note: as new external URLs are added to the codebase, more URLs will be wrapped with `getExternalLinkUrl` and this list may not always be up to date.
How to Override External URLs
-----------------------------
To override external URLs, you can use the frontend platform's configuration system.
This object should be added to the config object defined in the env.config.[js,jsx,ts,tsx], and must be named externalLinkUrlOverrides.
1. **Environment Configuration**
Add the URL overrides to your environment configuration:
.. code-block:: javascript
const config = {
// Other config options...
externalLinkUrlOverrides: {
'https://www.edx.org/accessibility': 'https://your-custom-domain.com/accessibility',
// Add other URL overrides here
}
};
Examples
--------
**Original URL:** Default community accessibility link
**Override:** Your institution's accessibility policy page
.. code-block:: javascript
// In your app configuration
getExternalLinkUrl('https://www.edx.org/accessibility')
// Returns: 'https://your-custom-domain.com/accessibility'
// Instead of the default Open edX community link
Benefits
--------
- **Customization**: Institutions can point to their own resources
- **Maintainability**: URLs can be changed without code modifications
- **Consistency**: Centralized URL management across the application
- **Flexibility**: Different environments can have different external links

View File

@@ -11,9 +11,11 @@ module.exports = createConfig('jest', {
],
moduleNameMapper: {
'^lodash-es$': 'lodash',
// This alias is for any code in the src directory that wants to avoid '../../' style relative imports:
'^@src/(.*)$': '<rootDir>/src/$1',
// This alias is used for plugins in the plugins/ folder only.
'^CourseAuthoring/(.*)$': '<rootDir>/src/$1',
},
modulePathIgnorePatterns: [
'/src/pages-and-resources/utils.test.jsx',
],
});

View File

@@ -1,11 +0,0 @@
# This file describes this Open edX repo, as described in OEP-2:
# http://open-edx-proposals.readthedocs.io/en/latest/oeps/oep-0002.html#specification
nick: cath
oeps: {}
owner: edx/platform-core-tnl
openedx-release:
# The openedx-release key is described in OEP-10:
# https://open-edx-proposals.readthedocs.io/en/latest/oep-0010-proc-openedx-releases.html
# The FAQ might also be helpful: https://openedx.atlassian.net/wiki/spaces/COMM/pages/1331268879/Open+edX+Release+FAQ
ref: master

21469
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -11,11 +11,11 @@
],
"scripts": {
"build": "fedx-scripts webpack",
"i18n_extract": "fedx-scripts formatjs extract",
"i18n_extract": "fedx-scripts formatjs extract --include=plugins",
"stylelint": "stylelint \"plugins/**/*.scss\" \"src/**/*.scss\" \"scss/**/*.scss\" --config .stylelintrc.json",
"lint": "npm run stylelint && fedx-scripts eslint --ext .js --ext .jsx --ext .ts --ext .tsx .",
"oxlint": "oxlint --type-aware --deny-warnings",
"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",
@@ -23,11 +23,6 @@
"test:ci": "TZ=UTC fedx-scripts jest --silent --coverage --passWithNoTests",
"types": "tsc --noEmit"
},
"husky": {
"hooks": {
"pre-commit": "npm run lint"
}
},
"author": "edX",
"license": "AGPL-3.0",
"homepage": "https://github.com/openedx/frontend-app-authoring#readme",
@@ -39,6 +34,7 @@
},
"dependencies": {
"@codemirror/lang-html": "^6.0.0",
"@codemirror/lang-markdown": "^6.0.0",
"@codemirror/lang-xml": "^6.0.0",
"@codemirror/lint": "^6.2.1",
"@codemirror/state": "^6.0.0",
@@ -48,12 +44,12 @@
"@dnd-kit/sortable": "^8.0.0",
"@dnd-kit/utilities": "^3.2.2",
"@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-platform": "^8.0.3",
"@edx/openedx-atlas": "^0.6.0",
"@edx/browserslist-config": "1.5.1",
"@edx/frontend-component-footer": "^14.9.0",
"@edx/frontend-component-header": "^8.1.0",
"@edx/frontend-enterprise-hotjar": "^7.2.0",
"@edx/frontend-platform": "^8.5.5",
"@edx/openedx-atlas": "^0.7.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,67 +60,66 @@
"@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-build": "^14.0.14",
"@openedx/frontend-plugin-framework": "^1.2.1",
"@openedx/paragon": "^22.8.1",
"@openedx/frontend-build": "^14.6.2",
"@openedx/frontend-plugin-framework": "^1.8.0",
"@openedx/paragon": "^23.5.0",
"@redux-devtools/extension": "^3.3.0",
"@reduxjs/toolkit": "1.9.7",
"@tanstack/react-query": "4.36.1",
"@tinymce/tinymce-react": "^3.14.0",
"@reduxjs/toolkit": "2.11.2",
"@tanstack/react-query": "5.90.21",
"@tinymce/tinymce-react": "^6.0.0",
"classnames": "2.5.1",
"codemirror": "^6.0.0",
"email-validator": "2.0.4",
"fast-xml-parser": "^4.0.10",
"fast-xml-parser": "^5.0.0",
"file-saver": "^2.0.5",
"formik": "2.4.6",
"formik": "2.4.9",
"frontend-components-tinymce-advanced-plugins": "^1.0.3",
"jszip": "^3.10.1",
"lodash": "4.17.21",
"lodash": "4.17.23",
"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": "^18.3.1",
"react-datepicker": "^8.10.0",
"react-dom": "^18.3.1",
"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.23.1",
"react-router-dom": "6.23.1",
"react-select": "5.8.0",
"react-responsive": "10.0.1",
"react-router": "6.30.3",
"react-router-dom": "6.30.3",
"react-select": "5.10.2",
"react-textarea-autosize": "^8.5.3",
"react-transition-group": "4.4.5",
"redux": "4.0.5",
"redux": "4.2.1",
"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",
"universal-cookie": "^8.0.0",
"uuid": "^11.1.0",
"xmlchecker": "^0.1.0",
"yup": "0.31.1"
"yup": "0.32.11"
},
"devDependencies": {
"@edx/react-unit-test-utils": "3.0.0",
"@edx/stylelint-config-edx": "2.3.3",
"@edx/typescript-config": "^1.0.1",
"@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",
"@types/lodash": "^4.17.7",
"axios-mock-adapter": "1.22.0",
"@testing-library/jest-dom": "^6.6.3",
"@testing-library/react": "^16.2.0",
"@testing-library/user-event": "^14.6.1",
"@types/lodash": "^4.17.17",
"@types/react": "^18",
"@types/react-dom": "^18",
"axios-mock-adapter": "2.1.0",
"eslint-import-resolver-webpack": "^0.13.8",
"fetch-mock-jest": "^1.5.1",
"husky": "7.0.4",
"jest-canvas-mock": "^2.5.2",
"jest-expect-message": "^1.1.3",
"react-test-renderer": "17.0.2",
"oxlint": "^1.42.0",
"oxlint-tsgolint": "^0.11.2",
"react-test-renderer": "^18.3.1",
"redux-mock-store": "^1.5.4"
}
}

View File

@@ -2,16 +2,12 @@ import React from 'react';
import { screen, waitFor } from '@testing-library/react';
import { RequestStatus } from 'CourseAuthoring/data/constants';
import { render } from 'CourseAuthoring/pages-and-resources/utils.test';
import { initializeMocks, render } from 'CourseAuthoring/testUtils';
import LearningAssistantSettings from './Settings';
const onClose = () => { };
describe('Learning Assistant Settings', () => {
beforeEach(() => {
jest.clearAllMocks();
});
it('renders', async () => {
const initialState = {
models: {
@@ -38,14 +34,8 @@ describe('Learning Assistant Settings', () => {
},
};
render(
<LearningAssistantSettings
onClose={onClose}
/>,
{
preloadedState: initialState,
},
);
initializeMocks({ initialState });
render(<LearningAssistantSettings onClose={onClose} />);
const toggleDescription = 'Reinforce learning concepts by sharing text-based course content '
+ 'with OpenAI (via API) to power an in-course Learning Assistant. Learners can leave feedback about the quality '

View File

@@ -1,6 +1,6 @@
import React, { useEffect, useState } from 'react';
import { getConfig } from '@edx/frontend-platform';
import { FormattedMessage, injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { getConfig, getExternalLinkUrl } from '@edx/frontend-platform';
import { FormattedMessage, useIntl } from '@edx/frontend-platform/i18n';
import { Form, Hyperlink } from '@openedx/paragon';
import PropTypes from 'prop-types';
import AppConfigFormDivider from 'CourseAuthoring/pages-and-resources/discussions/app-config-form/apps/shared/AppConfigFormDivider';
@@ -11,10 +11,10 @@ import LiveCommonFields from './LiveCommonFields';
import messages from './messages';
const BbbSettings = ({
intl,
values,
setFieldValue,
}) => {
const intl = useIntl();
const [bbbPlan, setBbbPlan] = useState(values.tierType);
useEffect(() => {
@@ -93,7 +93,7 @@ const BbbSettings = ({
<span data-testid="free-plan-message">
{intl.formatMessage(messages.freePlanMessage)}
<Hyperlink
destination="https://bigbluebutton.org/privacy-policy/"
destination={getExternalLinkUrl('https://bigbluebutton.org/privacy-policy/')}
target="_blank"
rel="noopener noreferrer"
showLaunchIcon
@@ -107,12 +107,10 @@ const BbbSettings = ({
)}
</>
</>
);
};
BbbSettings.propTypes = {
intl: intlShape.isRequired,
values: PropTypes.shape({
consumerKey: PropTypes.string,
consumerSecret: PropTypes.string,
@@ -127,4 +125,4 @@ BbbSettings.propTypes = {
setFieldValue: PropTypes.func.isRequired,
};
export default injectIntl(BbbSettings);
export default BbbSettings;

View File

@@ -4,21 +4,16 @@ import {
getByRole,
getAllByRole,
waitForElementToBeRemoved,
} from '@testing-library/react';
initializeMocks,
} from 'CourseAuthoring/testUtils';
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 userEvent from '@testing-library/user-event';
import initializeStore from 'CourseAuthoring/store';
import { executeThunk } from 'CourseAuthoring/utils';
import PagesAndResourcesProvider from 'CourseAuthoring/pages-and-resources/PagesAndResourcesProvider';
import { CourseAuthoringProvider } from 'CourseAuthoring/CourseAuthoringContext';
import LiveSettings from './Settings';
import {
generateLiveConfigurationApiResponse,
@@ -40,17 +35,20 @@ ReactDOM.createPortal = jest.fn(node => node);
const renderComponent = () => {
const wrapper = render(
<IntlProvider locale="en">
<AppProvider store={store} wrapWithRouter={false}>
<PagesAndResourcesProvider courseId={courseId}>
<MemoryRouter initialEntries={[liveSettingsUrl]}>
<Routes>
<Route path={liveSettingsUrl} element={<PageWrap><LiveSettings onClose={() => {}} /></PageWrap>} />
</Routes>
</MemoryRouter>
</PagesAndResourcesProvider>
</AppProvider>
</IntlProvider>,
<CourseAuthoringProvider courseId={courseId}>
<PagesAndResourcesProvider courseId={courseId}>
<LiveSettings onClose={() => {}} />
</PagesAndResourcesProvider>
</CourseAuthoringProvider>,
{
path: liveSettingsUrl,
routerProps: {
initialEntries: [liveSettingsUrl],
},
params: {
courseId,
},
},
);
container = wrapper.container;
};
@@ -74,16 +72,9 @@ const mockStore = async ({
describe('BBB Settings', () => {
beforeEach(async () => {
initializeMockApp({
authenticatedUser: {
userId: 3,
username: 'abc123',
administrator: false,
roles: [],
},
});
store = initializeStore(initialState);
axiosMock = new MockAdapter(getAuthenticatedHttpClient());
const mocks = initializeMocks({ initialState });
store = mocks.reduxStore;
axiosMock = mocks.axiosMock;
});
test('Plan dropdown to be visible and enabled in UI', async () => {
@@ -124,12 +115,13 @@ describe('BBB Settings', () => {
);
test('free plans message is visible when free plan is selected', async () => {
const user = userEvent.setup();
await mockStore({ emailSharing: true, isFreeTier: true });
renderComponent();
const spinner = getByRole(container, 'status');
await waitForElementToBeRemoved(spinner);
const dropDown = container.querySelector('select[name="tierType"]');
userEvent.selectOptions(
await user.selectOptions(
dropDown,
getByRole(dropDown, 'option', { name: 'Free' }),
);

View File

@@ -1,42 +1,43 @@
import React from 'react';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { useIntl } from '@edx/frontend-platform/i18n';
import PropTypes from 'prop-types';
import FormikControl from 'CourseAuthoring/generic/FormikControl';
import messages from './messages';
const LiveCommonFields = ({
intl,
values,
}) => (
<>
<p className="pb-2">{intl.formatMessage(messages.formInstructions)}</p>
<FormikControl
name="consumerKey"
value={values.consumerKey}
floatingLabel={intl.formatMessage(messages.consumerKey)}
className="pb-1"
type="input"
/>
<FormikControl
name="consumerSecret"
value={values.consumerSecret}
floatingLabel={intl.formatMessage(messages.consumerSecret)}
className="pb-1"
type="password"
/>
<FormikControl
name="launchUrl"
value={values.launchUrl}
floatingLabel={intl.formatMessage(messages.launchUrl)}
className="pb-1"
type="input"
/>
</>
);
}) => {
const intl = useIntl();
return (
<>
<p className="pb-2">{intl.formatMessage(messages.formInstructions)}</p>
<FormikControl
name="consumerKey"
value={values.consumerKey}
floatingLabel={intl.formatMessage(messages.consumerKey)}
className="pb-1"
type="input"
/>
<FormikControl
name="consumerSecret"
value={values.consumerSecret}
floatingLabel={intl.formatMessage(messages.consumerSecret)}
className="pb-1"
type="password"
/>
<FormikControl
name="launchUrl"
value={values.launchUrl}
floatingLabel={intl.formatMessage(messages.launchUrl)}
className="pb-1"
type="input"
/>
</>
);
};
LiveCommonFields.propTypes = {
intl: intlShape.isRequired,
values: PropTypes.shape({
consumerKey: PropTypes.string,
consumerSecret: PropTypes.string,
@@ -45,4 +46,4 @@ LiveCommonFields.propTypes = {
}).isRequired,
};
export default injectIntl(LiveCommonFields);
export default LiveCommonFields;

View File

@@ -1,8 +1,9 @@
// oxlint-disable unicorn/no-thenable
import React, { useEffect } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { camelCase } from 'lodash';
import { Icon } from '@openedx/paragon';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { useIntl } from '@edx/frontend-platform/i18n';
import PropTypes from 'prop-types';
import * as Yup from 'yup';
import { useNavigate } from 'react-router-dom';
@@ -11,6 +12,7 @@ import AppSettingsModal from 'CourseAuthoring/pages-and-resources/app-settings-m
import { useModel } from 'CourseAuthoring/generic/model-store';
import Loading from 'CourseAuthoring/generic/Loading';
import { RequestStatus } from 'CourseAuthoring/data/constants';
import { useCourseAuthoringContext } from 'CourseAuthoring/CourseAuthoringContext';
import { fetchLiveData, saveLiveConfiguration, saveLiveConfigurationAsDraft } from './data/thunks';
import { selectApp } from './data/slice';
@@ -20,12 +22,12 @@ import ZoomSettings from './ZoomSettings';
import BBBSettings from './BBBSettings';
const LiveSettings = ({
intl,
onClose,
}) => {
const intl = useIntl();
const navigate = useNavigate();
const dispatch = useDispatch();
const courseId = useSelector(state => state.courseDetail.courseId);
const { courseId } = useCourseAuthoringContext();
const availableProviders = useSelector((state) => state.live.appIds);
const {
piiSharingAllowed, selectedAppId, enabled, status,
@@ -71,6 +73,7 @@ const LiveSettings = ({
};
const handleSettingsSave = async (values) => {
// oxlint-disable-next-line @typescript-eslint/await-thenable - this dispatch() IS returning a promise.
await dispatch(saveLiveConfiguration(courseId, values, navigate));
};
@@ -130,8 +133,7 @@ const LiveSettings = ({
};
LiveSettings.propTypes = {
intl: intlShape.isRequired,
onClose: PropTypes.func.isRequired,
};
export default injectIntl(LiveSettings);
export default LiveSettings;

View File

@@ -8,20 +8,14 @@ import {
queryByText,
getByRole,
waitForElementToBeRemoved,
} from '@testing-library/react';
initializeMocks,
} from 'CourseAuthoring/testUtils';
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 { CourseAuthoringProvider } from 'CourseAuthoring/CourseAuthoringContext';
import LiveSettings from './Settings';
import {
generateLiveConfigurationApiResponse,
@@ -44,17 +38,20 @@ ReactDOM.createPortal = jest.fn(node => node);
const renderComponent = () => {
const wrapper = render(
<IntlProvider locale="en">
<AppProvider store={store} wrapWithRouter={false}>
<PagesAndResourcesProvider courseId={courseId}>
<MemoryRouter initialEntries={[liveSettingsUrl]}>
<Routes>
<Route path={liveSettingsUrl} element={<PageWrap><LiveSettings onClose={() => {}} /></PageWrap>} />
</Routes>
</MemoryRouter>
</PagesAndResourcesProvider>
</AppProvider>
</IntlProvider>,
<PagesAndResourcesProvider courseId={courseId}>
<CourseAuthoringProvider>
<LiveSettings onClose={() => {}} />
</CourseAuthoringProvider>
</PagesAndResourcesProvider>,
{
path: liveSettingsUrl,
routerProps: {
initialEntries: [liveSettingsUrl],
},
params: {
courseId,
},
},
);
container = wrapper.container;
};
@@ -77,16 +74,11 @@ const mockStore = async ({
describe('LiveSettings', () => {
beforeEach(async () => {
initializeMockApp({
authenticatedUser: {
userId: 3,
username: 'abc123',
administrator: false,
roles: [],
},
const mocks = initializeMocks({
initialState,
});
store = initializeStore(initialState);
axiosMock = new MockAdapter(getAuthenticatedHttpClient());
store = mocks.reduxStore;
axiosMock = mocks.axiosMock;
});
test('Live Configuration modal is visible', async () => {

View File

@@ -1,5 +1,5 @@
import React from 'react';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { useIntl } from '@edx/frontend-platform/i18n';
import PropTypes from 'prop-types';
import FormikControl from 'CourseAuthoring/generic/FormikControl';
@@ -8,37 +8,38 @@ import { providerNames } from './constants';
import LiveCommonFields from './LiveCommonFields';
const ZoomSettings = ({
intl,
values,
}) => (
// eslint-disable-next-line react/jsx-no-useless-fragment
<>
{!values.piiSharingEnable ? (
<p data-testid="request-pii-sharing">
{intl.formatMessage(messages.requestPiiSharingEnable, { provider: providerNames[values.provider] })}
</p>
) : (
<>
{(values.piiSharingEmail || values.piiSharingUsername)
&& (
<p data-testid="helper-text">
{intl.formatMessage(messages.providerHelperText, { providerName: providerNames[values.provider] })}
</p>
)}
<LiveCommonFields values={values} />
<FormikControl
name="launchEmail"
value={values.launchEmail}
floatingLabel={intl.formatMessage(messages.launchEmail)}
type="input"
/>
</>
)}
</>
);
}) => {
const intl = useIntl();
return (
// eslint-disable-next-line react/jsx-no-useless-fragment
<>
{!values.piiSharingEnable ? (
<p data-testid="request-pii-sharing">
{intl.formatMessage(messages.requestPiiSharingEnable, { provider: providerNames[values.provider] })}
</p>
) : (
<>
{(values.piiSharingEmail || values.piiSharingUsername)
&& (
<p data-testid="helper-text">
{intl.formatMessage(messages.providerHelperText, { providerName: providerNames[values.provider] })}
</p>
)}
<LiveCommonFields values={values} />
<FormikControl
name="launchEmail"
value={values.launchEmail}
floatingLabel={intl.formatMessage(messages.launchEmail)}
type="input"
/>
</>
)}
</>
);
};
ZoomSettings.propTypes = {
intl: intlShape.isRequired,
values: PropTypes.shape({
consumerKey: PropTypes.string,
consumerSecret: PropTypes.string,
@@ -51,4 +52,4 @@ ZoomSettings.propTypes = {
}).isRequired,
};
export default injectIntl(ZoomSettings);
export default ZoomSettings;

View File

@@ -3,19 +3,14 @@ import {
queryByTestId,
getByRole,
waitForElementToBeRemoved,
} from '@testing-library/react';
initializeMocks,
} from 'CourseAuthoring/testUtils';
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 { CourseAuthoringProvider } from 'CourseAuthoring/CourseAuthoringContext';
import LiveSettings from './Settings';
import {
generateLiveConfigurationApiResponse,
@@ -38,17 +33,20 @@ ReactDOM.createPortal = jest.fn(node => node);
const renderComponent = () => {
const wrapper = render(
<IntlProvider locale="en">
<AppProvider store={store} wrapWithRouter={false}>
<PagesAndResourcesProvider courseId={courseId}>
<MemoryRouter initialEntries={[liveSettingsUrl]}>
<Routes>
<Route path={liveSettingsUrl} element={<PageWrap><LiveSettings onClose={() => {}} /></PageWrap>} />
</Routes>
</MemoryRouter>
</PagesAndResourcesProvider>
</AppProvider>
</IntlProvider>,
<CourseAuthoringProvider courseId={courseId}>
<PagesAndResourcesProvider courseId={courseId}>
<LiveSettings onClose={() => {}} />
</PagesAndResourcesProvider>
</CourseAuthoringProvider>,
{
path: liveSettingsUrl,
routerProps: {
initialEntries: [liveSettingsUrl],
},
params: {
courseId,
},
},
);
container = wrapper.container;
};
@@ -71,16 +69,9 @@ const mockStore = async ({
describe('Zoom Settings', () => {
beforeEach(async () => {
initializeMockApp({
authenticatedUser: {
userId: 3,
username: 'abc123',
administrator: false,
roles: [],
},
});
store = initializeStore(initialState);
axiosMock = new MockAdapter(getAuthenticatedHttpClient());
const mocks = initializeMocks({ initialState });
store = mocks.reduxStore;
axiosMock = mocks.axiosMock;
});
test('LTI fields are visible when pii sharing is enabled', async () => {

View File

@@ -1,4 +1,3 @@
/* eslint-disable import/prefer-default-export */
import { ensureConfig, getConfig } from '@edx/frontend-platform';
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
import { bbbPlanTypes } from '../constants';

View File

@@ -48,8 +48,9 @@ const ORASettings = ({ onClose }) => {
event.preventDefault();
success = success && await handleSettingsSave(formValues);
await setSaveError(!success);
setSaveError(!success);
if ((initialFormValues.enableFlexiblePeerGrade !== formValues.enableFlexiblePeerGrade) && success) {
// oxlint-disable-next-line @typescript-eslint/await-thenable - this dispatch() IS returning a promise.
success = await dispatch(updateModel({
modelType: 'courseApps',
model: {

View File

@@ -125,10 +125,13 @@ describe('ORASettings', () => {
});
it('Displays title, helper text and badge when flexible peer grading button is enabled', async () => {
renderComponent();
await mockStore({ apiStatus: 200, enabled: true });
renderComponent();
waitFor(() => {
const checkbox = screen.getByRole('checkbox', { name: /Flex Peer Grading/ });
expect(checkbox).toBeChecked();
await waitFor(() => {
const label = screen.getByText(messages.enableFlexPeerGradeLabel.defaultMessage);
const enableBadge = screen.getByTestId('enable-badge');
@@ -142,7 +145,7 @@ describe('ORASettings', () => {
renderComponent();
await mockStore({ apiStatus: 200, enabled: false });
const label = screen.getByText(messages.enableFlexPeerGradeLabel.defaultMessage);
const label = await screen.findByText(messages.enableFlexPeerGradeLabel.defaultMessage);
const enableBadge = screen.queryByTestId('enable-badge');
expect(label).toBeVisible();

View File

@@ -8,7 +8,7 @@ import PropTypes from 'prop-types';
import { getConfig } from '@edx/frontend-platform';
import { getAuthenticatedUser } from '@edx/frontend-platform/auth';
import { injectIntl, intlShape, FormattedMessage } from '@edx/frontend-platform/i18n';
import { useIntl, FormattedMessage } from '@edx/frontend-platform/i18n';
import {
ActionRow, Alert, Badge, Form, Hyperlink, ModalDialog, StatefulButton,
} from '@openedx/paragon';
@@ -22,16 +22,17 @@ import { useModel } from 'CourseAuthoring/generic/model-store';
import PermissionDeniedAlert from 'CourseAuthoring/generic/PermissionDeniedAlert';
import { useIsMobile } from 'CourseAuthoring/utils';
import { PagesAndResourcesContext } from 'CourseAuthoring/pages-and-resources/PagesAndResourcesProvider';
import { useCourseAuthoringContext } from 'CourseAuthoring/CourseAuthoringContext';
import messages from './messages';
const ProctoringSettings = ({ intl, onClose }) => {
const ProctoringSettings = ({ onClose }) => {
const intl = useIntl();
const initialFormValues = {
enableProctoredExams: false,
proctoringProvider: false,
escalationEmail: '',
allowOptingOut: false,
createZendeskTickets: false,
};
const [formValues, setFormValues] = useState(initialFormValues);
const [loading, setLoading] = useState(true);
@@ -40,6 +41,7 @@ const ProctoringSettings = ({ intl, onClose }) => {
const [loadingPermissionError, setLoadingPermissionError] = useState(false);
const [allowLtiProviders, setAllowLtiProviders] = useState(false);
const [availableProctoringProviders, setAvailableProctoringProviders] = useState([]);
const [requiresEscalationEmailProviders, setRequiresEscalationEmailProviders] = useState([]);
const [ltiProctoringProviders, setLtiProctoringProviders] = useState([]);
const [courseStartDate, setCourseStartDate] = useState('');
const [saveSuccess, setSaveSuccess] = useState(false);
@@ -64,7 +66,7 @@ const ProctoringSettings = ({ intl, onClose }) => {
}
const { courseId } = useContext(PagesAndResourcesContext);
const courseDetails = useModel('courseDetails', courseId);
const { courseDetails } = useCourseAuthoringContext();
const org = courseDetails?.org;
const appInfo = useModel('courseApps', 'proctoring');
const alertRef = React.createRef();
@@ -77,18 +79,14 @@ const ProctoringSettings = ({ intl, onClose }) => {
const value = target.type === 'checkbox' ? target.checked : target.value;
const { name } = target;
if (['allowOptingOut', 'createZendeskTickets'].includes(name)) {
if (['allowOptingOut'].includes(name)) {
// Form.Radio expects string values, so convert back to a boolean here
setFormValues({ ...formValues, [name]: value === 'true' });
} else if (name === 'proctoringProvider') {
const newFormValues = { ...formValues, proctoringProvider: value };
if (value === 'proctortrack') {
setFormValues({ ...newFormValues, createZendeskTickets: false });
if (requiresEscalationEmailProviders.includes(value)) {
setFormValues({ ...newFormValues });
setShowEscalationEmail(true);
} else if (value === 'software_secure') {
setFormValues({ ...newFormValues, createZendeskTickets: true });
setShowEscalationEmail(false);
} else if (isLtiProvider(value)) {
setFormValues(newFormValues);
setShowEscalationEmail(true);
@@ -115,14 +113,13 @@ const ProctoringSettings = ({ intl, onClose }) => {
enable_proctored_exams: formValues.enableProctoredExams,
// lti providers are managed outside edx-platform, lti_external indicates this
proctoring_provider: isLtiProviderSelected ? 'lti_external' : selectedProvider,
create_zendesk_tickets: formValues.createZendeskTickets,
},
};
if (isEdxStaff) {
studioDataToPostBack.proctored_exam_settings.allow_proctoring_opt_out = formValues.allowOptingOut;
}
if (formValues.proctoringProvider === 'proctortrack') {
if (requiresEscalationEmailProviders.includes(formValues.proctoringProvider)) {
studioDataToPostBack.proctored_exam_settings.proctoring_escalation_email = formValues.escalationEmail === '' ? null : formValues.escalationEmail;
}
@@ -159,7 +156,7 @@ const ProctoringSettings = ({ intl, onClose }) => {
event.preventDefault();
const isLtiProviderSelected = isLtiProvider(formValues.proctoringProvider);
if (
(formValues.proctoringProvider === 'proctortrack' || isLtiProviderSelected)
(requiresEscalationEmailProviders.includes(formValues.proctoringProvider) || isLtiProviderSelected)
&& !EmailValidator.validate(formValues.escalationEmail)
&& !(formValues.escalationEmail === '' && !formValues.enableProctoredExams)
) {
@@ -386,29 +383,6 @@ const ProctoringSettings = ({ intl, onClose }) => {
</Form.Group>
</fieldset>
)}
{/* CREATE ZENDESK TICKETS */}
{ isEdxStaff && formValues.enableProctoredExams && !isLtiProviderSelected && (
<fieldset aria-describedby="createZendeskTicketsText">
<Form.Group controlId="formCreateZendeskTickets">
<Form.Label as="legend" className="font-weight-bold">
{intl.formatMessage(messages['authoring.proctoring.createzendesk.label'])}
</Form.Label>
<Form.RadioSet
name="createZendeskTickets"
value={formValues.createZendeskTickets.toString()}
onChange={handleChange}
>
<Form.Radio value="true" data-testid="createZendeskTicketsYes">
{intl.formatMessage(messages['authoring.proctoring.yes'])}
</Form.Radio>
<Form.Radio value="false" data-testid="createZendeskTicketsNo">
{intl.formatMessage(messages['authoring.proctoring.no'])}
</Form.Radio>
</Form.RadioSet>
</Form.Group>
</fieldset>
)}
</>
);
}
@@ -526,6 +500,7 @@ const ProctoringSettings = ({ intl, onClose }) => {
setSubmissionInProgress(false);
setCourseStartDate(settingsResponse.data.course_start_date);
setAvailableProctoringProviders(settingsResponse.data.available_proctoring_providers);
setRequiresEscalationEmailProviders(settingsResponse.data.requires_escalation_email_providers);
// The list of providers returned by studio settings are the default behavior. If lti_external
// is available as an option display the list of LTI providers returned by the exam service.
@@ -553,10 +528,11 @@ const ProctoringSettings = ({ intl, onClose }) => {
selectedProvider = proctoredExamSettings.proctoring_provider;
}
const isProctortrack = selectedProvider === 'proctortrack';
const requiresEscalationEmailProvidersList = settingsResponse.data.requires_escalation_email_providers;
const isEscalationEmailRequired = requiresEscalationEmailProvidersList.includes(selectedProvider);
const ltiProviderSelected = proctoringProvidersLti.some(p => p.name === selectedProvider);
if (isProctortrack || ltiProviderSelected) {
if (isEscalationEmailRequired || ltiProviderSelected) {
setShowEscalationEmail(true);
}
@@ -569,7 +545,6 @@ const ProctoringSettings = ({ intl, onClose }) => {
proctoringProvider: selectedProvider,
enableProctoredExams: proctoredExamSettings.enable_proctored_exams,
allowOptingOut: proctoredExamSettings.allow_proctoring_opt_out,
createZendeskTickets: proctoredExamSettings.create_zendesk_tickets,
// The backend API may return null for the proctoringEscalationEmail value, which is the default.
// In order to keep our email input component controlled, we use the empty string as the default
// and perform this conversion during GETs and POSTs.
@@ -652,10 +627,9 @@ const ProctoringSettings = ({ intl, onClose }) => {
};
ProctoringSettings.propTypes = {
intl: intlShape.isRequired,
onClose: PropTypes.func.isRequired,
};
ProctoringSettings.defaultProps = {};
export default injectIntl(ProctoringSettings);
export default ProctoringSettings;

View File

@@ -1,18 +1,15 @@
import React from 'react';
import {
render, screen, cleanup, waitFor, fireEvent, act,
} from '@testing-library/react';
import MockAdapter from 'axios-mock-adapter';
initializeMocks,
} from 'CourseAuthoring/testUtils';
import { initializeMockApp, mergeConfig } from '@edx/frontend-platform';
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
import { IntlProvider, injectIntl } from '@edx/frontend-platform/i18n';
import { AppProvider } from '@edx/frontend-platform/react';
import { mergeConfig } from '@edx/frontend-platform';
import StudioApiService from 'CourseAuthoring/data/services/StudioApiService';
import ExamsApiService from 'CourseAuthoring/data/services/ExamsApiService';
import initializeStore from 'CourseAuthoring/store';
import PagesAndResourcesProvider from 'CourseAuthoring/pages-and-resources/PagesAndResourcesProvider';
import { CourseAuthoringProvider } from 'CourseAuthoring/CourseAuthoringContext';
import { getCourseDetailsUrl } from 'CourseAuthoring/data/api';
import ProctoredExamSettings from './Settings';
const courseId = 'course-v1%3AedX%2BDemoX%2BDemo_Course';
@@ -20,57 +17,57 @@ const defaultProps = {
courseId,
onClose: () => {},
};
const IntlProctoredExamSettings = injectIntl(ProctoredExamSettings);
let store;
const intlWrapper = children => (
<AppProvider store={store}>
const renderComponent = children => (
<CourseAuthoringProvider courseId={defaultProps.courseId}>
<PagesAndResourcesProvider courseId={defaultProps.courseId}>
<IntlProvider locale="en">
{children}
</IntlProvider>
{children}
</PagesAndResourcesProvider>
</AppProvider>
</CourseAuthoringProvider>
);
let axiosMock;
describe('ProctoredExamSettings', () => {
/**
* @param {boolean} isAdmin
* @param {string | undefined} org
*/
function setupApp(isAdmin = true, org = undefined) {
mergeConfig({
EXAMS_BASE_URL: 'http://exams.testing.co',
}, 'CourseAuthoringConfig');
initializeMockApp({
authenticatedUser: {
userId: 3,
username: 'abc123',
administrator: isAdmin,
roles: [],
},
});
store = initializeStore({
models: {
courseApps: {
proctoring: {},
},
courseDetails: {
[courseId]: {
start: Date(),
const user = {
userId: 3,
username: 'abc123',
administrator: isAdmin,
roles: [],
};
const mocks = initializeMocks({
user,
initialState: {
models: {
courseApps: {
proctoring: {},
},
},
...(org ? { courseDetails: { [courseId]: { org } } } : {}),
},
});
axiosMock = new MockAdapter(getAuthenticatedHttpClient());
axiosMock.onGet(
`${ExamsApiService.getExamsBaseUrl()}/api/v1/providers${org ? `?org=${org}` : ''}`,
).reply(200, [
{
name: 'test_lti',
verbose_name: 'LTI Provider',
},
]);
axiosMock = mocks.axiosMock;
axiosMock
.onGet(getCourseDetailsUrl(courseId, user.username))
.reply(200, {
courseId,
name: 'Course Test',
start: Date(),
...(org ? { org } : {}),
});
axiosMock.onGet(`${ExamsApiService.getExamsBaseUrl()}/api/v1/providers`)
.reply(200, [{ name: 'test_lti', verbose_name: 'LTI Provider' }]);
if (org) {
axiosMock.onGet(`${ExamsApiService.getExamsBaseUrl()}/api/v1/providers?org=${org}`)
.reply(200, [{ name: 'test_lti', verbose_name: 'LTI Provider' }]);
}
axiosMock.onGet(
`${ExamsApiService.getExamsBaseUrl()}/api/v1/configs/course_id/${defaultProps.courseId}`,
).reply(200, {
@@ -85,54 +82,20 @@ describe('ProctoredExamSettings', () => {
allow_proctoring_opt_out: false,
proctoring_provider: 'mockproc',
proctoring_escalation_email: 'test@example.com',
create_zendesk_tickets: true,
},
available_proctoring_providers: ['software_secure', 'proctortrack', 'mockproc', 'lti_external'],
available_proctoring_providers: ['software_secure', 'mockproc', 'lti_external'],
requires_escalation_email_providers: ['test_lti'],
course_start_date: '2070-01-01T00:00:00Z',
});
}
afterEach(() => {
cleanup();
axiosMock.reset();
});
beforeEach(async () => {
setupApp();
});
describe('Field dependencies', () => {
beforeEach(async () => {
await act(async () => render(intlWrapper(<IntlProctoredExamSettings {...defaultProps} />)));
});
it('Updates Zendesk ticket field if proctortrack is provider', async () => {
await waitFor(() => {
screen.getByDisplayValue('mockproc');
});
const selectElement = screen.getByDisplayValue('mockproc');
fireEvent.change(selectElement, { target: { value: 'proctortrack' } });
const zendeskTicketInput = screen.getByTestId('createZendeskTicketsNo');
expect(zendeskTicketInput.checked).toEqual(true);
});
it('Updates Zendesk ticket field if software_secure is provider', async () => {
await waitFor(() => {
screen.getByDisplayValue('mockproc');
});
const selectElement = screen.getByDisplayValue('mockproc');
fireEvent.change(selectElement, { target: { value: 'software_secure' } });
const zendeskTicketInput = screen.getByTestId('createZendeskTicketsYes');
expect(zendeskTicketInput.checked).toEqual(true);
});
it('Does not update zendesk ticket field for any other provider', async () => {
await waitFor(() => {
screen.getByDisplayValue('mockproc');
});
const selectElement = screen.getByDisplayValue('mockproc');
fireEvent.change(selectElement, { target: { value: 'mockproc' } });
const zendeskTicketInput = screen.getByTestId('createZendeskTicketsYes');
expect(zendeskTicketInput.checked).toEqual(true);
await act(async () => render(renderComponent(<ProctoredExamSettings {...defaultProps} />)));
});
it('Hides all other fields when enabledProctorExam is false when first loaded', async () => {
@@ -146,13 +109,13 @@ describe('ProctoredExamSettings', () => {
allow_proctoring_opt_out: false,
proctoring_provider: 'mockproc',
proctoring_escalation_email: 'test@example.com',
create_zendesk_tickets: true,
},
available_proctoring_providers: ['software_secure', 'proctortrack', 'mockproc'],
available_proctoring_providers: ['software_secure', 'mockproc'],
requires_escalation_email_providers: [],
course_start_date: '2070-01-01T00:00:00Z',
});
await act(async () => render(intlWrapper(<IntlProctoredExamSettings {...defaultProps} />)));
await act(async () => render(renderComponent(<ProctoredExamSettings {...defaultProps} />)));
await waitFor(() => {
screen.getByText('Proctored exams');
});
@@ -161,8 +124,6 @@ describe('ProctoredExamSettings', () => {
expect(screen.queryByText('Allow Opting Out of Proctored Exams')).toBeNull();
expect(screen.queryByDisplayValue('mockproc')).toBeNull();
expect(screen.queryByTestId('escalationEmail')).toBeNull();
expect(screen.queryByTestId('createZendeskTicketsYes')).toBeNull();
expect(screen.queryByTestId('createZendeskTicketsNo')).toBeNull();
});
it('Hides all other fields when enableProctoredExams toggled to false', async () => {
@@ -172,8 +133,6 @@ describe('ProctoredExamSettings', () => {
expect(screen.queryByText('Allow opting out of proctored exams')).toBeDefined();
expect(screen.queryByDisplayValue('mockproc')).toBeDefined();
expect(screen.queryByTestId('escalationEmail')).toBeDefined();
expect(screen.queryByTestId('createZendeskTicketsYes')).toBeDefined();
expect(screen.queryByTestId('createZendeskTicketsNo')).toBeDefined();
let enabledProctoredExamCheck = screen.getAllByLabelText('Proctored exams', { exact: false })[0];
expect(enabledProctoredExamCheck.checked).toEqual(true);
@@ -183,8 +142,6 @@ describe('ProctoredExamSettings', () => {
expect(screen.queryByText('Allow opting out of proctored exams')).toBeNull();
expect(screen.queryByDisplayValue('mockproc')).toBeNull();
expect(screen.queryByTestId('escalationEmail')).toBeNull();
expect(screen.queryByTestId('createZendeskTicketsYes')).toBeNull();
expect(screen.queryByTestId('createZendeskTicketsNo')).toBeNull();
});
it('Hides unsupported fields when lti provider is selected', async () => {
@@ -194,13 +151,11 @@ describe('ProctoredExamSettings', () => {
const selectElement = screen.getByDisplayValue('mockproc');
fireEvent.change(selectElement, { target: { value: 'test_lti' } });
expect(screen.queryByTestId('allowOptingOutRadio')).toBeNull();
expect(screen.queryByTestId('createZendeskTicketsYes')).toBeNull();
expect(screen.queryByTestId('createZendeskTicketsNo')).toBeNull();
});
});
describe('Validation with invalid escalation email', () => {
const proctoringProvidersRequiringEscalationEmail = ['proctortrack', 'test_lti'];
const proctoringProvidersRequiringEscalationEmail = ['test_lti'];
beforeEach(async () => {
axiosMock.onGet(
@@ -209,14 +164,21 @@ describe('ProctoredExamSettings', () => {
proctored_exam_settings: {
enable_proctored_exams: true,
allow_proctoring_opt_out: false,
proctoring_provider: 'proctortrack',
proctoring_provider: 'lti_external',
proctoring_escalation_email: 'test@example.com',
create_zendesk_tickets: true,
},
available_proctoring_providers: ['software_secure', 'proctortrack', 'mockproc', 'lti_external'],
available_proctoring_providers: ['software_secure', 'mockproc', 'lti_external'],
requires_escalation_email_providers: ['test_lti'],
course_start_date: '2070-01-01T00:00:00Z',
});
axiosMock.onGet(
`${ExamsApiService.getExamsBaseUrl()}/api/v1/configs/course_id/${defaultProps.courseId}`,
).reply(200, {
provider: 'test_lti',
escalation_email: 'test@example.com',
});
axiosMock.onPatch(
ExamsApiService.getExamConfigurationUrl(defaultProps.courseId),
).reply(204, {});
@@ -225,13 +187,13 @@ describe('ProctoredExamSettings', () => {
StudioApiService.getProctoredExamSettingsUrl(defaultProps.courseId),
).reply(200, {});
await act(async () => render(intlWrapper(<IntlProctoredExamSettings {...defaultProps} />)));
await act(async () => render(renderComponent(<ProctoredExamSettings {...defaultProps} />)));
});
proctoringProvidersRequiringEscalationEmail.forEach(provider => {
it(`Creates an alert when no proctoring escalation email is provided with ${provider} selected`, async () => {
await waitFor(() => {
screen.getByDisplayValue('proctortrack');
screen.getByDisplayValue('LTI Provider');
});
const selectEscalationEmailElement = screen.getByDisplayValue('test@example.com');
fireEvent.change(selectEscalationEmailElement, { target: { value: '' } });
@@ -252,10 +214,10 @@ describe('ProctoredExamSettings', () => {
it(`Creates an alert when invalid proctoring escalation email is provided with ${provider} selected`, async () => {
await waitFor(() => {
screen.getByDisplayValue('proctortrack');
screen.getByDisplayValue('LTI Provider');
});
const selectElement = screen.getByDisplayValue('proctortrack');
const selectElement = screen.getByDisplayValue('LTI Provider');
fireEvent.change(selectElement, { target: { value: provider } });
const selectEscalationEmailElement = screen.getByDisplayValue('test@example.com');
@@ -278,7 +240,7 @@ describe('ProctoredExamSettings', () => {
it('Creates an alert when invalid proctoring escalation email is provided with proctoring disabled', async () => {
await waitFor(() => {
screen.getByDisplayValue('proctortrack');
screen.getByDisplayValue('LTI Provider');
});
const selectEscalationEmailElement = screen.getByDisplayValue('test@example.com');
fireEvent.change(selectEscalationEmailElement, { target: { value: 'foo.bar' } });
@@ -296,7 +258,7 @@ describe('ProctoredExamSettings', () => {
it('Has no error when empty proctoring escalation email is provided with proctoring disabled', async () => {
await waitFor(() => {
screen.getByDisplayValue('proctortrack');
screen.getByDisplayValue('LTI Provider');
});
const selectEscalationEmailElement = screen.getByDisplayValue('test@example.com');
fireEvent.change(selectEscalationEmailElement, { target: { value: '' } });
@@ -319,7 +281,7 @@ describe('ProctoredExamSettings', () => {
it(`Has no error when valid proctoring escalation email is provided with ${provider} selected`, async () => {
await waitFor(() => {
screen.getByDisplayValue('proctortrack');
screen.getByDisplayValue('LTI Provider');
});
const selectEscalationEmailElement = screen.getByDisplayValue('test@example.com');
fireEvent.change(selectEscalationEmailElement, { target: { value: 'foo@bar.com' } });
@@ -340,9 +302,9 @@ describe('ProctoredExamSettings', () => {
it(`Escalation email field hidden when proctoring backend is not ${provider}`, async () => {
await waitFor(() => {
screen.getByDisplayValue('proctortrack');
screen.getByDisplayValue('LTI Provider');
});
const proctoringBackendSelect = screen.getByDisplayValue('proctortrack');
const proctoringBackendSelect = screen.getByDisplayValue('LTI Provider');
const selectEscalationEmailElement = screen.getByTestId('escalationEmail');
expect(selectEscalationEmailElement.value).toEqual('test@example.com');
fireEvent.change(proctoringBackendSelect, { target: { value: 'software_secure' } });
@@ -351,13 +313,13 @@ describe('ProctoredExamSettings', () => {
it(`Escalation email Field Show when proctoring backend is switched back to ${provider}`, async () => {
await waitFor(() => {
screen.getByDisplayValue('proctortrack');
screen.getByDisplayValue('LTI Provider');
});
const proctoringBackendSelect = screen.getByDisplayValue('proctortrack');
const proctoringBackendSelect = screen.getByDisplayValue('LTI Provider');
let selectEscalationEmailElement = screen.getByTestId('escalationEmail');
fireEvent.change(proctoringBackendSelect, { target: { value: 'software_secure' } });
expect(screen.queryByTestId('escalationEmail')).toBeNull();
fireEvent.change(proctoringBackendSelect, { target: { value: 'proctortrack' } });
fireEvent.change(proctoringBackendSelect, { target: { value: provider } });
expect(screen.queryByTestId('escalationEmail')).toBeDefined();
selectEscalationEmailElement = screen.getByTestId('escalationEmail');
expect(selectEscalationEmailElement.value).toEqual('test@example.com');
@@ -365,7 +327,7 @@ describe('ProctoredExamSettings', () => {
it('Submits form when "Enter" key is hit in the escalation email field', async () => {
await waitFor(() => {
screen.getByDisplayValue('proctortrack');
screen.getByDisplayValue('LTI Provider');
});
const selectEscalationEmailElement = screen.getByDisplayValue('test@example.com');
fireEvent.change(selectEscalationEmailElement, { target: { value: '' } });
@@ -383,9 +345,9 @@ describe('ProctoredExamSettings', () => {
allow_proctoring_opt_out: false,
proctoring_provider: 'mockproc',
proctoring_escalation_email: 'test@example.com',
create_zendesk_tickets: true,
},
available_proctoring_providers: ['software_secure', 'proctortrack', 'mockproc'],
available_proctoring_providers: ['software_secure', 'mockproc'],
requires_escalation_email_providers: [],
course_start_date: '2099-01-01T00:00:00Z',
};
@@ -395,9 +357,9 @@ describe('ProctoredExamSettings', () => {
allow_proctoring_opt_out: false,
proctoring_provider: 'mockproc',
proctoring_escalation_email: 'test@example.com',
create_zendesk_tickets: true,
},
available_proctoring_providers: ['software_secure', 'proctortrack', 'mockproc'],
available_proctoring_providers: ['software_secure', 'mockproc'],
requires_escalation_email_providers: [],
course_start_date: '2013-01-01T00:00:00Z',
};
@@ -409,8 +371,8 @@ describe('ProctoredExamSettings', () => {
const isAdmin = false;
setupApp(isAdmin);
mockCourseData(mockGetPastCourseData);
await act(async () => render(intlWrapper(<IntlProctoredExamSettings {...defaultProps} />)));
const providerOption = screen.getByTestId('proctortrack');
await act(async () => render(renderComponent(<ProctoredExamSettings {...defaultProps} />)));
const providerOption = screen.getByTestId('software_secure');
expect(providerOption.hasAttribute('disabled')).toEqual(true);
});
@@ -418,8 +380,8 @@ describe('ProctoredExamSettings', () => {
const isAdmin = false;
setupApp(isAdmin);
mockCourseData(mockGetFutureCourseData);
await act(async () => render(intlWrapper(<IntlProctoredExamSettings {...defaultProps} />)));
const providerOption = screen.getByTestId('proctortrack');
await act(async () => render(renderComponent(<ProctoredExamSettings {...defaultProps} />)));
const providerOption = screen.getByTestId('software_secure');
expect(providerOption.hasAttribute('disabled')).toEqual(false);
});
@@ -428,8 +390,8 @@ describe('ProctoredExamSettings', () => {
const org = 'test-org';
setupApp(isAdmin, org);
mockCourseData(mockGetFutureCourseData);
await act(async () => render(intlWrapper(<IntlProctoredExamSettings {...defaultProps} />)));
const providerOption = screen.getByTestId('proctortrack');
await act(async () => render(renderComponent(<ProctoredExamSettings {...defaultProps} />)));
const providerOption = screen.getByTestId('software_secure');
expect(providerOption.hasAttribute('disabled')).toEqual(false);
});
@@ -437,8 +399,8 @@ describe('ProctoredExamSettings', () => {
const isAdmin = true;
setupApp(isAdmin);
mockCourseData(mockGetPastCourseData);
await act(async () => render(intlWrapper(<IntlProctoredExamSettings {...defaultProps} />)));
const providerOption = screen.getByTestId('proctortrack');
await act(async () => render(renderComponent(<ProctoredExamSettings {...defaultProps} />)));
const providerOption = screen.getByTestId('software_secure');
expect(providerOption.hasAttribute('disabled')).toEqual(false);
});
@@ -446,18 +408,18 @@ describe('ProctoredExamSettings', () => {
const isAdmin = true;
setupApp(isAdmin);
mockCourseData(mockGetFutureCourseData);
await act(async () => render(intlWrapper(<IntlProctoredExamSettings {...defaultProps} />)));
const providerOption = screen.getByTestId('proctortrack');
await act(async () => render(renderComponent(<ProctoredExamSettings {...defaultProps} />)));
const providerOption = screen.getByTestId('software_secure');
expect(providerOption.hasAttribute('disabled')).toEqual(false);
});
it('Does not include lti_external as a selectable option', async () => {
const courseData = {
...mockGetFutureCourseData,
available_proctoring_providers: ['lti_external', 'proctortrack', 'mockproc'],
available_proctoring_providers: ['lti_external', 'mockproc'],
};
mockCourseData(courseData);
await act(async () => render(intlWrapper(<IntlProctoredExamSettings {...defaultProps} />)));
await act(async () => render(renderComponent(<ProctoredExamSettings {...defaultProps} />)));
await waitFor(() => {
screen.getByDisplayValue('mockproc');
});
@@ -467,10 +429,10 @@ describe('ProctoredExamSettings', () => {
it('Includes lti proctoring provider options when lti_external is allowed by studio', async () => {
const courseData = {
...mockGetFutureCourseData,
available_proctoring_providers: ['lti_external', 'proctortrack', 'mockproc'],
available_proctoring_providers: ['lti_external', 'mockproc'],
};
mockCourseData(courseData);
await act(async () => render(intlWrapper(<IntlProctoredExamSettings {...defaultProps} />)));
await act(async () => render(renderComponent(<ProctoredExamSettings {...defaultProps} />)));
await waitFor(() => {
screen.getByDisplayValue('mockproc');
});
@@ -483,7 +445,7 @@ describe('ProctoredExamSettings', () => {
const isAdmin = true;
setupApp(isAdmin);
mockCourseData(mockGetFutureCourseData);
await act(async () => render(intlWrapper(<IntlProctoredExamSettings {...defaultProps} />)));
await act(async () => render(renderComponent(<ProctoredExamSettings {...defaultProps} />)));
await waitFor(() => {
screen.getByDisplayValue('mockproc');
});
@@ -497,18 +459,20 @@ describe('ProctoredExamSettings', () => {
EXAMS_BASE_URL: null,
}, 'CourseAuthoringConfig');
await act(async () => render(intlWrapper(<IntlProctoredExamSettings {...defaultProps} />)));
await act(async () => render(renderComponent(<ProctoredExamSettings {...defaultProps} />)));
await waitFor(() => {
screen.getByDisplayValue('mockproc');
});
// only outgoing request should be for studio settings
expect(axiosMock.history.get.length).toBe(1);
// (1) for studio settings
// (2) waffle flags
// (3) for course details
expect(axiosMock.history.get.length).toBe(3);
expect(axiosMock.history.get[0].url.includes('proctored_exam_settings')).toEqual(true);
});
it('Selected LTI proctoring provider is shown on page load', async () => {
const courseData = { ...mockGetFutureCourseData };
courseData.available_proctoring_providers = ['lti_external', 'proctortrack', 'mockproc'];
courseData.available_proctoring_providers = ['lti_external', 'mockproc'];
courseData.proctored_exam_settings.proctoring_provider = 'lti_external';
mockCourseData(courseData);
axiosMock.onGet(
@@ -516,7 +480,7 @@ describe('ProctoredExamSettings', () => {
).reply(200, {
provider: 'test_lti',
});
await act(async () => render(intlWrapper(<IntlProctoredExamSettings {...defaultProps} />)));
await act(async () => render(renderComponent(<ProctoredExamSettings {...defaultProps} />)));
await waitFor(() => {
screen.getByText('Proctoring provider');
});
@@ -527,29 +491,24 @@ describe('ProctoredExamSettings', () => {
});
describe('Toggles field visibility based on user permissions', () => {
it('Hides opting out and zendesk tickets for non edX staff', async () => {
it('Hides opting out for non edX staff', async () => {
setupApp(false);
await act(async () => render(intlWrapper(<IntlProctoredExamSettings {...defaultProps} />)));
await act(async () => render(renderComponent(<ProctoredExamSettings {...defaultProps} />)));
expect(screen.queryByTestId('allowOptingOutYes')).toBeNull();
expect(screen.queryByTestId('createZendeskTicketsYes')).toBeNull();
});
it('Shows opting out and zendesk tickets for edX staff', async () => {
it('Shows opting out for edX staff', async () => {
setupApp(true);
await act(async () => render(intlWrapper(<IntlProctoredExamSettings {...defaultProps} />)));
await act(async () => render(renderComponent(<ProctoredExamSettings {...defaultProps} />)));
expect(screen.queryByTestId('allowOptingOutYes')).not.toBeNull();
expect(screen.queryByTestId('createZendeskTicketsYes')).not.toBeNull();
});
});
describe('Connection states', () => {
it('Shows the spinner before the connection is complete', async () => {
await act(async () => {
render(intlWrapper(<IntlProctoredExamSettings {...defaultProps} />));
// This expectation is _inside_ the `act` intentionally, so that it executes immediately.
const spinner = screen.getByRole('status');
expect(spinner.textContent).toEqual('Loading...');
});
render(renderComponent(<ProctoredExamSettings {...defaultProps} />));
const spinner = await screen.findByRole('status');
expect(spinner.textContent).toEqual('Loading...');
});
it('Show connection error message when we suffer studio server side error', async () => {
@@ -557,7 +516,7 @@ describe('ProctoredExamSettings', () => {
StudioApiService.getProctoredExamSettingsUrl(defaultProps.courseId),
).reply(500);
await act(async () => render(intlWrapper(<IntlProctoredExamSettings {...defaultProps} />)));
await act(async () => render(renderComponent(<ProctoredExamSettings {...defaultProps} />)));
const connectionError = screen.getByTestId('connectionErrorAlert');
expect(connectionError.textContent).toEqual(
expect.stringContaining('We encountered a technical error when loading this page.'),
@@ -569,7 +528,7 @@ describe('ProctoredExamSettings', () => {
`${ExamsApiService.getExamsBaseUrl()}/api/v1/providers`,
).reply(500);
await act(async () => render(intlWrapper(<IntlProctoredExamSettings {...defaultProps} />)));
await act(async () => render(renderComponent(<ProctoredExamSettings {...defaultProps} />)));
const connectionError = screen.getByTestId('connectionErrorAlert');
expect(connectionError.textContent).toEqual(
expect.stringContaining('We encountered a technical error when loading this page.'),
@@ -581,7 +540,7 @@ describe('ProctoredExamSettings', () => {
StudioApiService.getProctoredExamSettingsUrl(defaultProps.courseId),
).reply(403);
await act(async () => render(intlWrapper(<IntlProctoredExamSettings {...defaultProps} />)));
await act(async () => render(renderComponent(<ProctoredExamSettings {...defaultProps} />)));
const permissionError = screen.getByTestId('permissionDeniedAlert');
expect(permissionError.textContent).toEqual(
expect.stringContaining('You are not authorized to view this page'),
@@ -600,7 +559,7 @@ describe('ProctoredExamSettings', () => {
});
it('Disable button while submitting', async () => {
await act(async () => render(intlWrapper(<IntlProctoredExamSettings {...defaultProps} />)));
await act(async () => render(renderComponent(<ProctoredExamSettings {...defaultProps} />)));
let submitButton = screen.getByTestId('submissionButton');
expect(screen.queryByTestId('saveInProgress')).toBeFalsy();
fireEvent.click(submitButton);
@@ -609,15 +568,30 @@ describe('ProctoredExamSettings', () => {
expect(submitButton).toHaveAttribute('disabled');
});
it('Makes API call successfully with proctoring_escalation_email if proctortrack', async () => {
await act(async () => render(intlWrapper(<IntlProctoredExamSettings {...defaultProps} />)));
// Make a change to the provider to proctortrack and set the email
it('Makes API call successfully with proctoring_escalation_email if test_lti', async () => {
// Setup mock to include test_lti as available provider
axiosMock.onGet(
StudioApiService.getProctoredExamSettingsUrl(defaultProps.courseId),
).reply(200, {
proctored_exam_settings: {
enable_proctored_exams: true,
allow_proctoring_opt_out: false,
proctoring_provider: 'mockproc',
proctoring_escalation_email: 'test@example.com',
},
available_proctoring_providers: ['software_secure', 'mockproc', 'lti_external'],
requires_escalation_email_providers: ['test_lti'],
course_start_date: '2070-01-01T00:00:00Z',
});
await act(async () => render(renderComponent(<ProctoredExamSettings {...defaultProps} />)));
// Make a change to the provider to test_lti and set the email
const selectElement = screen.getByDisplayValue('mockproc');
fireEvent.change(selectElement, { target: { value: 'proctortrack' } });
fireEvent.change(selectElement, { target: { value: 'test_lti' } });
const escalationEmail = screen.getByTestId('escalationEmail');
expect(escalationEmail.value).toEqual('test@example.com');
fireEvent.change(escalationEmail, { target: { value: 'proctortrack@example.com' } });
expect(escalationEmail.value).toEqual('proctortrack@example.com');
fireEvent.change(escalationEmail, { target: { value: 'test_lti@example.com' } });
expect(escalationEmail.value).toEqual('test_lti@example.com');
const submitButton = screen.getByTestId('submissionButton');
fireEvent.click(submitButton);
expect(axiosMock.history.post.length).toBe(1);
@@ -625,11 +599,15 @@ describe('ProctoredExamSettings', () => {
proctored_exam_settings: {
enable_proctored_exams: true,
allow_proctoring_opt_out: false,
proctoring_provider: 'proctortrack',
proctoring_escalation_email: 'proctortrack@example.com',
create_zendesk_tickets: false,
proctoring_provider: 'lti_external',
proctoring_escalation_email: 'test_lti@example.com',
},
});
expect(axiosMock.history.patch.length).toBe(1);
expect(JSON.parse(axiosMock.history.patch[0].data)).toEqual({
provider: 'test_lti',
escalation_email: 'test_lti@example.com',
});
await waitFor(() => {
const errorAlert = screen.getByTestId('saveSuccess');
@@ -640,10 +618,10 @@ describe('ProctoredExamSettings', () => {
});
});
it('Makes API call successfully without proctoring_escalation_email if not proctortrack', async () => {
await act(async () => render(intlWrapper(<IntlProctoredExamSettings {...defaultProps} />)));
it('Makes API call successfully without proctoring_escalation_email if not requiring escalation email', async () => {
await act(async () => render(renderComponent(<ProctoredExamSettings {...defaultProps} />)));
// make sure we have not selected proctortrack as the proctoring provider
// make sure we have not selected a provider requiring escalation email
expect(screen.getByDisplayValue('mockproc')).toBeDefined();
const submitButton = screen.getByTestId('submissionButton');
@@ -654,7 +632,6 @@ describe('ProctoredExamSettings', () => {
enable_proctored_exams: true,
allow_proctoring_opt_out: false,
proctoring_provider: 'mockproc',
create_zendesk_tickets: true,
},
});
@@ -668,7 +645,7 @@ describe('ProctoredExamSettings', () => {
});
it('Successfully updates exam configuration and studio provider is set to "lti_external" for lti providers', async () => {
await act(async () => render(intlWrapper(<IntlProctoredExamSettings {...defaultProps} />)));
await act(async () => render(renderComponent(<ProctoredExamSettings {...defaultProps} />)));
// Make a change to the provider to test_lti and set the email
const selectElement = screen.getByDisplayValue('mockproc');
fireEvent.change(selectElement, { target: { value: 'test_lti' } });
@@ -695,7 +672,7 @@ describe('ProctoredExamSettings', () => {
enable_proctored_exams: true,
allow_proctoring_opt_out: false,
proctoring_provider: 'lti_external',
create_zendesk_tickets: true,
proctoring_escalation_email: 'test_lti@example.com',
},
});
@@ -709,7 +686,7 @@ describe('ProctoredExamSettings', () => {
});
it('Sets exam service provider to null if a non-lti provider is selected', async () => {
await act(async () => render(intlWrapper(<IntlProctoredExamSettings {...defaultProps} />)));
await act(async () => render(renderComponent(<ProctoredExamSettings {...defaultProps} />)));
const submitButton = screen.getByTestId('submissionButton');
fireEvent.click(submitButton);
// update exam service config
@@ -725,7 +702,6 @@ describe('ProctoredExamSettings', () => {
enable_proctored_exams: true,
allow_proctoring_opt_out: false,
proctoring_provider: 'mockproc',
create_zendesk_tickets: true,
},
});
@@ -747,13 +723,13 @@ describe('ProctoredExamSettings', () => {
allow_proctoring_opt_out: false,
proctoring_provider: 'mockproc',
proctoring_escalation_email: 'test@example.com',
create_zendesk_tickets: true,
},
available_proctoring_providers: ['software_secure', 'proctortrack', 'mockproc'],
available_proctoring_providers: ['software_secure', 'mockproc'],
requires_escalation_email_providers: [],
course_start_date: '2070-01-01T00:00:00Z',
});
await act(async () => render(intlWrapper(<IntlProctoredExamSettings {...defaultProps} />)));
await act(async () => render(renderComponent(<ProctoredExamSettings {...defaultProps} />)));
const submitButton = screen.getByTestId('submissionButton');
fireEvent.click(submitButton);
// does not update exam service config
@@ -765,7 +741,6 @@ describe('ProctoredExamSettings', () => {
enable_proctored_exams: true,
allow_proctoring_opt_out: false,
proctoring_provider: 'mockproc',
create_zendesk_tickets: true,
},
});
@@ -783,7 +758,7 @@ describe('ProctoredExamSettings', () => {
StudioApiService.getProctoredExamSettingsUrl(defaultProps.courseId),
).reply(500);
await act(async () => render(intlWrapper(<IntlProctoredExamSettings {...defaultProps} />)));
await act(async () => render(renderComponent(<ProctoredExamSettings {...defaultProps} />)));
const submitButton = screen.getByTestId('submissionButton');
fireEvent.click(submitButton);
expect(axiosMock.history.post.length).toBe(1);
@@ -801,7 +776,7 @@ describe('ProctoredExamSettings', () => {
`${ExamsApiService.getExamsBaseUrl()}/api/v1/configs/course_id/${defaultProps.courseId}`,
).reply(500, 'error');
await act(async () => render(intlWrapper(<IntlProctoredExamSettings {...defaultProps} />)));
await act(async () => render(renderComponent(<ProctoredExamSettings {...defaultProps} />)));
const submitButton = screen.getByTestId('submissionButton');
fireEvent.click(submitButton);
expect(axiosMock.history.post.length).toBe(1);
@@ -819,7 +794,7 @@ describe('ProctoredExamSettings', () => {
`${ExamsApiService.getExamsBaseUrl()}/api/v1/configs/course_id/${defaultProps.courseId}`,
).reply(403, 'error');
await act(async () => render(intlWrapper(<IntlProctoredExamSettings {...defaultProps} />)));
await act(async () => render(renderComponent(<ProctoredExamSettings {...defaultProps} />)));
const submitButton = screen.getByTestId('submissionButton');
fireEvent.click(submitButton);
expect(axiosMock.history.post.length).toBe(1);
@@ -838,7 +813,7 @@ describe('ProctoredExamSettings', () => {
StudioApiService.getProctoredExamSettingsUrl(defaultProps.courseId),
).reply(500);
await act(async () => render(intlWrapper(<IntlProctoredExamSettings {...defaultProps} />)));
await act(async () => render(renderComponent(<ProctoredExamSettings {...defaultProps} />)));
const submitButton = screen.getByTestId('submissionButton');
fireEvent.click(submitButton);
expect(axiosMock.history.post.length).toBe(1);
@@ -865,27 +840,5 @@ describe('ProctoredExamSettings', () => {
expect(document.activeElement).toEqual(successAlert);
});
});
it('Include Zendesk ticket in post request if user is not an admin', async () => {
// use non-admin user for test
const isAdmin = false;
setupApp(isAdmin);
await act(async () => render(intlWrapper(<IntlProctoredExamSettings {...defaultProps} />)));
// Make a change to the proctoring provider
const selectElement = screen.getByDisplayValue('mockproc');
fireEvent.change(selectElement, { target: { value: 'proctortrack' } });
const submitButton = screen.getByTestId('submissionButton');
fireEvent.click(submitButton);
expect(axiosMock.history.post.length).toBe(1);
expect(JSON.parse(axiosMock.history.post[0].data)).toEqual({
proctored_exam_settings: {
enable_proctored_exams: true,
proctoring_provider: 'proctortrack',
proctoring_escalation_email: 'test@example.com',
create_zendesk_tickets: false,
},
});
});
});
});

View File

@@ -1,11 +1,6 @@
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.',
@@ -86,11 +81,6 @@ const messages = defineMessages({
defaultMessage: 'Allow learners to opt out of proctoring on proctored exams',
description: 'Label for radio selection allowing proctored exam opt out',
},
'authoring.proctoring.createzendesk.label': {
id: 'authoring.proctoring.createzendesk.label',
defaultMessage: 'Create Zendesk tickets for suspicious attempts',
description: 'Label for Zendesk ticket creation radio select.',
},
'authoring.proctoring.error.single': {
id: 'authoring.proctoring.error.single',
defaultMessage: 'There is 1 error in this form.',

View File

@@ -1,4 +1,4 @@
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { useIntl } from '@edx/frontend-platform/i18n';
import PropTypes from 'prop-types';
import React from 'react';
import * as Yup from 'yup';
@@ -8,7 +8,8 @@ import { useAppSetting } from 'CourseAuthoring/utils';
import AppSettingsModal from 'CourseAuthoring/pages-and-resources/app-settings-modal/AppSettingsModal';
import messages from './messages';
const ProgressSettings = ({ intl, onClose }) => {
const ProgressSettings = ({ onClose }) => {
const intl = useIntl();
const [disableProgressGraph, saveSetting] = useAppSetting('disableProgressGraph');
const showProgressGraphSetting = getConfig().ENABLE_PROGRESS_GRAPH_SETTINGS.toString().toLowerCase() === 'true';
@@ -48,8 +49,7 @@ const ProgressSettings = ({ intl, onClose }) => {
};
ProgressSettings.propTypes = {
intl: intlShape.isRequired,
onClose: PropTypes.func.isRequired,
};
export default injectIntl(ProgressSettings);
export default ProgressSettings;

View File

@@ -1,4 +1,4 @@
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { useIntl } from '@edx/frontend-platform/i18n';
import { Button, Form, TransitionReplace } from '@openedx/paragon';
import PropTypes from 'prop-types';
import React, { useState } from 'react';
@@ -30,8 +30,9 @@ const TeamTypeNameMessage = {
};
const GroupEditor = ({
intl, group, onDelete, onChange, onBlur, fieldNameCommonBase, errors,
group, onDelete, onChange, onBlur, fieldNameCommonBase, errors,
}) => {
const intl = useIntl();
const [isDeleting, setDeleting] = useState(false);
const [isOpen, setOpen] = useState(group.id === null);
const initiateDeletion = () => setDeleting(true);
@@ -149,7 +150,6 @@ export const groupShape = PropTypes.shape({
});
GroupEditor.propTypes = {
intl: intlShape.isRequired,
fieldNameCommonBase: PropTypes.string.isRequired,
errors: PropTypes.shape({
name: PropTypes.string,
@@ -170,4 +170,4 @@ GroupEditor.defaultProps = {
},
};
export default injectIntl(GroupEditor);
export default GroupEditor;

View File

@@ -1,4 +1,4 @@
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { useIntl } from '@edx/frontend-platform/i18n';
import { Button, Form } from '@openedx/paragon';
import { Add } from '@openedx/paragon/icons';
@@ -17,15 +17,16 @@ import messages from './messages';
setupYupExtensions();
const TeamSettings = ({
intl,
onClose,
}) => {
const intl = useIntl();
const [teamsConfiguration, saveSettings] = useAppSetting('teamsConfiguration');
const blankNewGroup = {
name: '',
description: '',
type: GroupTypes.OPEN,
maxTeamSize: null,
userPartitionId: null,
id: null,
key: uuid(),
};
@@ -38,6 +39,7 @@ const TeamSettings = ({
type: group.type,
description: group.description,
max_team_size: group.maxTeamSize,
user_partition_id: group.userPartitionId,
}));
return saveSettings({
team_sets: groups,
@@ -105,6 +107,7 @@ const TeamSettings = ({
)
.when('enabled', {
is: true,
// oxlint-disable-next-line unicorn/no-thenable
then: Yup.array().min(1),
})
.default([])
@@ -164,8 +167,7 @@ const TeamSettings = ({
};
TeamSettings.propTypes = {
intl: intlShape.isRequired,
onClose: PropTypes.func.isRequired,
};
export default injectIntl(TeamSettings);
export default TeamSettings;

View File

@@ -1,4 +1,3 @@
/* eslint-disable import/prefer-default-export */
import { getConfig } from '@edx/frontend-platform';
import { GroupTypes } from 'CourseAuthoring/data/constants';

View File

@@ -1,4 +1,4 @@
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { useIntl } from '@edx/frontend-platform/i18n';
import PropTypes from 'prop-types';
import React from 'react';
import * as Yup from 'yup';
@@ -8,7 +8,8 @@ import { useAppSetting } from 'CourseAuthoring/utils';
import AppSettingsModal from 'CourseAuthoring/pages-and-resources/app-settings-modal/AppSettingsModal';
import messages from './messages';
const WikiSettings = ({ intl, onClose }) => {
const WikiSettings = ({ onClose }) => {
const intl = useIntl();
const [enablePublicWiki, saveSetting] = useAppSetting('allowPublicWikiAccess');
const handleSettingsSave = (values) => saveSetting(values.enablePublicWiki);
@@ -32,7 +33,7 @@ const WikiSettings = ({ intl, onClose }) => {
label={intl.formatMessage(messages.enablePublicWikiLabel)}
helpText={intl.formatMessage(messages.enablePublicWikiHelp)}
onChange={handleChange}
onBlue={handleBlur}
onBlur={handleBlur}
checked={values.enablePublicWiki}
/>
)
@@ -42,8 +43,7 @@ const WikiSettings = ({ intl, onClose }) => {
};
WikiSettings.propTypes = {
intl: intlShape.isRequired,
onClose: PropTypes.func.isRequired,
};
export default injectIntl(WikiSettings);
export default WikiSettings;

View File

@@ -1,7 +1,6 @@
import React, { useCallback, useContext, useEffect } from 'react';
import { useDispatch } from 'react-redux';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { useIntl } from '@edx/frontend-platform/i18n';
import { PagesAndResourcesContext } from 'CourseAuthoring/pages-and-resources/PagesAndResourcesProvider';
import { useNavigate } from 'react-router-dom';
@@ -10,7 +9,8 @@ import messages from './messages';
import { fetchXpertSettings } from './data/thunks';
const XpertUnitSummarySettings = ({ intl }) => {
const XpertUnitSummarySettings = () => {
const intl = useIntl();
const { path: pagesAndResourcesPath, courseId } = useContext(PagesAndResourcesContext);
const dispatch = useDispatch();
const navigate = useNavigate();
@@ -38,8 +38,4 @@ const XpertUnitSummarySettings = ({ intl }) => {
);
};
XpertUnitSummarySettings.propTypes = {
intl: intlShape.isRequired,
};
export default injectIntl(XpertUnitSummarySettings);
export default XpertUnitSummarySettings;

View File

@@ -7,7 +7,7 @@ import {
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
import { AppProvider, PageWrap } from '@edx/frontend-platform/react';
import {
queryByTestId, render, waitFor, getByText, fireEvent,
findByTestId, queryByTestId, render, waitFor, getByText, fireEvent,
} from '@testing-library/react';
import MockAdapter from 'axios-mock-adapter';
import PagesAndResourcesProvider from 'CourseAuthoring/pages-and-resources/PagesAndResourcesProvider';
@@ -106,8 +106,9 @@ describe('XpertUnitSummarySettings', () => {
});
test('Shows switch on if enabled from backend', async () => {
const enableBadge = await findByTestId(container, 'enable-badge');
expect(container.querySelector('#enable-xpert-unit-summary-toggle').checked).toBeTruthy();
expect(queryByTestId(container, 'enable-badge')).toBeTruthy();
expect(enableBadge).toBeTruthy();
});
test('Shows switch on if disabled from backend', async () => {

View File

@@ -19,7 +19,7 @@ export function updateXpertSettings(courseId, state) {
}
dispatch(updateSavingStatus({ status: RequestStatus.FAILED }));
return false;
} catch (error) {
} catch {
dispatch(updateSavingStatus({ status: RequestStatus.FAILED }));
return false;
}
@@ -33,7 +33,7 @@ export function fetchXpertPluginConfigurable(courseId) {
try {
const { response } = await getXpertPluginConfigurable(courseId);
enabled = response?.enabled;
} catch (e) {
} catch {
enabled = undefined;
}
@@ -55,7 +55,7 @@ export function fetchXpertSettings(courseId) {
try {
const { response } = await getXpertSettings(courseId);
enabled = response?.enabled;
} catch (e) {
} catch {
enabled = undefined;
}
@@ -86,7 +86,7 @@ export function removeXpertSettings(courseId) {
}
dispatch(updateSavingStatus({ status: RequestStatus.FAILED }));
return false;
} catch (error) {
} catch {
dispatch(updateSavingStatus({ status: RequestStatus.FAILED }));
return false;
}
@@ -105,7 +105,7 @@ export function resetXpertSettings(courseId, state) {
}
dispatch(updateResetStatus({ status: RequestStatus.FAILED }));
return false;
} catch (error) {
} catch {
dispatch(updateResetStatus({ status: RequestStatus.FAILED }));
return false;
}

View File

@@ -1,4 +1,5 @@
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { useIntl } from '@edx/frontend-platform/i18n';
import { getExternalLinkUrl } from '@edx/frontend-platform';
import {
ActionRow,
Alert,
@@ -70,38 +71,40 @@ AppSettingsForm.defaultProps = {
};
const SettingsModalBase = ({
intl, title, onClose, variant, isMobile, children, footer,
}) => (
<ModalDialog
title={title}
isOpen
onClose={onClose}
size="lg"
variant={variant}
hasCloseButton={isMobile}
isFullscreenOnMobile
>
<ModalDialog.Header>
<ModalDialog.Title data-testid="modal-title">
{title}
</ModalDialog.Title>
</ModalDialog.Header>
<ModalDialog.Body>
{children}
</ModalDialog.Body>
<ModalDialog.Footer className="p-4">
<ActionRow>
<ModalDialog.CloseButton variant="tertiary">
{intl.formatMessage(messages.cancel)}
</ModalDialog.CloseButton>
{footer}
</ActionRow>
</ModalDialog.Footer>
</ModalDialog>
);
title, onClose, variant, isMobile, children, footer,
}) => {
const intl = useIntl();
return (
<ModalDialog
title={title}
isOpen
onClose={onClose}
size="lg"
variant={variant}
hasCloseButton={isMobile}
isFullscreenOnMobile
>
<ModalDialog.Header>
<ModalDialog.Title data-testid="modal-title">
{title}
</ModalDialog.Title>
</ModalDialog.Header>
<ModalDialog.Body>
{children}
</ModalDialog.Body>
<ModalDialog.Footer className="p-4">
<ActionRow>
<ModalDialog.CloseButton variant="tertiary">
{intl.formatMessage(messages.cancel)}
</ModalDialog.CloseButton>
{footer}
</ActionRow>
</ModalDialog.Footer>
</ModalDialog>
);
};
SettingsModalBase.propTypes = {
intl: intlShape.isRequired,
title: PropTypes.string.isRequired,
onClose: PropTypes.func.isRequired,
variant: PropTypes.oneOf(['default', 'dark']).isRequired,
@@ -115,11 +118,11 @@ SettingsModalBase.defaultProps = {
};
const ResetUnitsButton = ({
intl,
courseId,
checked,
visible,
}) => {
const intl = useIntl();
const resetStatusRequestStatus = useSelector(getResetStatus);
const dispatch = useDispatch();
@@ -185,7 +188,6 @@ const ResetUnitsButton = ({
};
ResetUnitsButton.propTypes = {
intl: intlShape.isRequired,
courseId: PropTypes.string.isRequired,
checked: PropTypes.oneOf(['true', 'false']).isRequired,
visible: PropTypes.bool,
@@ -196,7 +198,6 @@ ResetUnitsButton.defaultProps = {
};
const SettingsModal = ({
intl,
appId,
title,
children,
@@ -213,6 +214,7 @@ const SettingsModal = ({
allUnitsEnabledText,
noUnitsEnabledText,
}) => {
const intl = useIntl();
const { courseId } = useContext(PagesAndResourcesContext);
const loadingStatus = useSelector(getLoadingStatus);
const updateSettingsRequestStatus = useSelector(getSavingStatus);
@@ -237,8 +239,10 @@ const SettingsModal = ({
const values = { ...rest, enabled: enabled ? checked === 'true' : undefined };
if (enabled) {
// oxlint-disable-next-line @typescript-eslint/await-thenable - this dispatch() IS returning a promise.
success = await dispatch(updateXpertSettings(courseId, values));
} else {
// oxlint-disable-next-line @typescript-eslint/await-thenable - this dispatch() IS returning a promise.
success = await dispatch(removeXpertSettings(courseId));
}
@@ -275,7 +279,7 @@ const SettingsModal = ({
<div className="py-1">
<Hyperlink
className="text-primary-500"
destination="https://openai.com/api-data-privacy"
destination={getExternalLinkUrl('https://openai.com/api-data-privacy')}
target="_blank"
rel="noreferrer noopener"
>
@@ -372,7 +376,6 @@ const SettingsModal = ({
>
{allUnitsEnabledText}
<ResetUnitsButton
intl={intl}
courseId={courseId}
checked={formikProps.values.checked}
visible={formikProps.values.checked === 'true'}
@@ -385,7 +388,6 @@ const SettingsModal = ({
>
{noUnitsEnabledText}
<ResetUnitsButton
intl={intl}
courseId={courseId}
checked={formikProps.values.checked}
visible={formikProps.values.checked === 'false'}
@@ -423,7 +425,6 @@ const SettingsModal = ({
};
SettingsModal.propTypes = {
intl: intlShape.isRequired,
title: PropTypes.string.isRequired,
appId: PropTypes.string.isRequired,
children: PropTypes.func,
@@ -450,4 +451,4 @@ SettingsModal.defaultProps = {
enableReinitialize: false,
};
export default injectIntl(SettingsModal);
export default SettingsModal;

View File

@@ -1,5 +1,4 @@
@import "~@edx/brand/paragon/variables";
@import "~@openedx/paragon/scss/core/utilities-only";
@import "~@openedx/paragon/styles/scss/core/utilities-only";
.summary-radio {
display: flex;

View File

@@ -0,0 +1,219 @@
import { getConfig } from '@edx/frontend-platform';
import {
createContext, useContext, useMemo, useState,
} from 'react';
import { getAuthenticatedUser } from '@edx/frontend-platform/auth';
import { useCreateCourseBlock } from '@src/course-outline/data/apiHooks';
import { getCourseItem } from '@src/course-outline/data/api';
import { useDispatch, useSelector } from 'react-redux';
import {
addSection, addSubsection, addUnit, updateSavingStatus,
} from '@src/course-outline/data/slice';
import { useNavigate } from 'react-router';
import { getOutlineIndexData } from '@src/course-outline/data/selectors';
import { useToggleWithValue } from '@src/hooks';
import { SelectionState, type UnitXBlock, type XBlock } from '@src/data/types';
import { CourseDetailsData } from './data/api';
import { useCourseDetails, useWaffleFlags } from './data/apiHooks';
import { RequestStatus, RequestStatusType } from './data/constants';
type ModalState = {
value: XBlock | UnitXBlock;
subsectionId?: string;
sectionId?: string;
};
export type CourseAuthoringContextData = {
/** The ID of the current course */
courseId: string;
courseUsageKey: string;
courseDetails?: CourseDetailsData;
courseDetailStatus: RequestStatusType;
canChangeProviders: boolean;
handleAddSection: ReturnType<typeof useCreateCourseBlock>;
handleAddSubsection: ReturnType<typeof useCreateCourseBlock>;
handleAddAndOpenUnit: ReturnType<typeof useCreateCourseBlock>;
handleAddUnit: ReturnType<typeof useCreateCourseBlock>;
openUnitPage: (locator: string) => void;
getUnitUrl: (locator: string) => string;
isUnlinkModalOpen: boolean;
currentUnlinkModalData?: ModalState;
openUnlinkModal: (value: ModalState) => void;
closeUnlinkModal: () => void;
isPublishModalOpen: boolean;
currentPublishModalData?: ModalState;
openPublishModal: (value: ModalState) => void;
closePublishModal: () => void;
currentSelection?: SelectionState;
setCurrentSelection: React.Dispatch<React.SetStateAction<SelectionState | undefined>>;
};
/**
* Course Authoring Context.
* Always available when we're in the context of a single course.
*
* Get this using `useCourseAuthoringContext()`
*
*/
const CourseAuthoringContext = createContext<CourseAuthoringContextData | undefined>(undefined);
type CourseAuthoringProviderProps = {
children?: React.ReactNode;
courseId: string;
};
export const CourseAuthoringProvider = ({
children,
courseId,
}: CourseAuthoringProviderProps) => {
const dispatch = useDispatch();
const navigate = useNavigate();
const waffleFlags = useWaffleFlags();
const { data: courseDetails, status: courseDetailStatus } = useCourseDetails(courseId);
const canChangeProviders = getAuthenticatedUser().administrator || new Date(courseDetails?.start ?? 0) > new Date();
const { courseStructure } = useSelector(getOutlineIndexData);
const { id: courseUsageKey } = courseStructure || {};
const [
isUnlinkModalOpen,
currentUnlinkModalData,
openUnlinkModal,
closeUnlinkModal,
] = useToggleWithValue<ModalState>();
const [
isPublishModalOpen,
currentPublishModalData,
openPublishModal,
closePublishModal,
] = useToggleWithValue<ModalState>();
/**
* This will hold the state of current item that is being operated on,
* For example:
* - the details of container that is being edited.
* - the details of container of which see more dropdown is open.
* It is mostly used in modals which should be soon be replaced with its equivalent in sidebar.
*/
const [currentSelection, setCurrentSelection] = useState<SelectionState | undefined>();
const getUnitUrl = (locator: string) => {
if (getConfig().ENABLE_UNIT_PAGE === 'true' && waffleFlags.useNewUnitPage) {
// instanbul ignore next
return `/course/${courseId}/container/${locator}`;
}
return `${getConfig().STUDIO_BASE_URL}/container/${locator}`;
};
/**
* Open the unit page for a given locator.
*/
const openUnitPage = async (locator: string) => {
const url = getUnitUrl(locator);
if (getConfig().ENABLE_UNIT_PAGE === 'true' && waffleFlags.useNewUnitPage) {
// instanbul ignore next
navigate(url);
} else {
window.location.assign(url);
}
};
const addSectionToCourse = /* istanbul ignore next */ async (locator: string) => {
try {
const data = await getCourseItem(locator);
// Page should scroll to newly added section.
data.shouldScroll = true;
dispatch(addSection(data));
} catch {
dispatch(updateSavingStatus({ status: RequestStatus.FAILED }));
}
};
const addSubsectionToCourse = /* istanbul ignore next */ async (locator: string, parentLocator: string) => {
try {
const data = await getCourseItem(locator);
// Page should scroll to newly added subsection.
data.shouldScroll = true;
dispatch(addSubsection({ parentLocator, data }));
} catch {
dispatch(updateSavingStatus({ status: RequestStatus.FAILED }));
}
};
const addUnitToCourse = /* istanbul ignore next */ async (locator: string, parentLocator: string) => {
try {
const data = await getCourseItem(locator);
// Page should scroll to newly added subsection.
data.shouldScroll = true;
dispatch(addUnit({ parentLocator, data }));
} catch {
dispatch(updateSavingStatus({ status: RequestStatus.FAILED }));
}
};
const handleAddSection = useCreateCourseBlock(addSectionToCourse);
const handleAddSubsection = useCreateCourseBlock(addSubsectionToCourse);
/**
* import a unit block from library and redirect user to this unit page.
*/
const handleAddAndOpenUnit = useCreateCourseBlock(openUnitPage);
const handleAddUnit = useCreateCourseBlock(addUnitToCourse);
const context = useMemo<CourseAuthoringContextData>(() => ({
courseId,
courseUsageKey,
courseDetails,
courseDetailStatus,
canChangeProviders,
handleAddSection,
handleAddSubsection,
handleAddUnit,
handleAddAndOpenUnit,
getUnitUrl,
openUnitPage,
isUnlinkModalOpen,
openUnlinkModal,
closeUnlinkModal,
currentUnlinkModalData,
isPublishModalOpen,
currentPublishModalData,
openPublishModal,
closePublishModal,
currentSelection,
setCurrentSelection,
}), [
courseId,
courseUsageKey,
courseDetails,
courseDetailStatus,
canChangeProviders,
handleAddSection,
handleAddSubsection,
handleAddUnit,
handleAddAndOpenUnit,
getUnitUrl,
openUnitPage,
isUnlinkModalOpen,
openUnlinkModal,
closeUnlinkModal,
currentUnlinkModalData,
isPublishModalOpen,
currentPublishModalData,
openPublishModal,
closePublishModal,
currentSelection,
setCurrentSelection,
]);
return (
<CourseAuthoringContext.Provider value={context}>
{children}
</CourseAuthoringContext.Provider>
);
};
export function useCourseAuthoringContext(): CourseAuthoringContextData {
const ctx = useContext(CourseAuthoringContext);
if (ctx === undefined) {
/* istanbul ignore next */
throw new Error('useCourseAuthoringContext() was used in a component without a <CourseAuthoringProvider> ancestor.');
}
return ctx;
}

View File

@@ -1,18 +1,12 @@
import React from 'react';
import { getConfig } from '@edx/frontend-platform';
import { render } from '@testing-library/react';
import { getConfig, initializeMockApp } from '@edx/frontend-platform';
import MockAdapter from 'axios-mock-adapter';
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
import { AppProvider } from '@edx/frontend-platform/react';
import { IntlProvider } from '@edx/frontend-platform/i18n';
import initializeStore from './store';
import CourseAuthoringPage from './CourseAuthoringPage';
import PagesAndResources from './pages-and-resources/PagesAndResources';
import { executeThunk } from './utils';
import { fetchCourseApps } from './pages-and-resources/data/thunks';
import { fetchCourseDetail } from './data/thunks';
import { getApiWaffleFlagsUrl } from './data/api';
import { initializeMocks, render } from './testUtils';
import { CourseAuthoringProvider } from './CourseAuthoringContext';
const courseId = 'course-v1:edX+TestX+Test_Course';
let mockPathname = '/evilguy/';
@@ -25,17 +19,19 @@ jest.mock('react-router-dom', () => ({
let axiosMock;
let store;
beforeEach(() => {
initializeMockApp({
authenticatedUser: {
userId: 3,
username: 'abc123',
administrator: true,
roles: [],
},
});
store = initializeStore();
axiosMock = new MockAdapter(getAuthenticatedHttpClient());
const renderComponent = children => render(
<CourseAuthoringProvider courseId={courseId}>
{children}
</CourseAuthoringProvider>,
);
beforeEach(async () => {
const mocks = initializeMocks();
store = mocks.reduxStore;
axiosMock = mocks.axiosMock;
axiosMock
.onGet(getApiWaffleFlagsUrl(courseId))
.reply(200, {});
});
describe('Editor Pages Load no header', () => {
@@ -45,19 +41,14 @@ describe('Editor Pages Load no header', () => {
axiosMock.onGet(`${courseAppsApiUrl}/${courseId}`).reply(200, {
response: { status: 200 },
});
await executeThunk(fetchCourseApps(courseId), store.dispatch);
};
test('renders no loading wheel on editor pages', async () => {
mockPathname = '/editor/';
await mockStoreSuccess();
const wrapper = render(
<AppProvider store={store}>
<IntlProvider locale="en">
<CourseAuthoringPage courseId={courseId}>
<PagesAndResources courseId={courseId} />
</CourseAuthoringPage>
</IntlProvider>
</AppProvider>
const wrapper = renderComponent(
<CourseAuthoringPage>
<PagesAndResources />
</CourseAuthoringPage>
,
);
expect(wrapper.queryByRole('status')).not.toBeInTheDocument();
@@ -65,14 +56,10 @@ describe('Editor Pages Load no header', () => {
test('renders loading wheel on non editor pages', async () => {
mockPathname = '/evilguy/';
await mockStoreSuccess();
const wrapper = render(
<AppProvider store={store}>
<IntlProvider locale="en">
<CourseAuthoringPage courseId={courseId}>
<PagesAndResources courseId={courseId} />
</CourseAuthoringPage>
</IntlProvider>
</AppProvider>
const wrapper = renderComponent(
<CourseAuthoringPage>
<PagesAndResources />
</CourseAuthoringPage>
,
);
expect(wrapper.queryByRole('status')).toBeInTheDocument();
@@ -88,7 +75,6 @@ describe('Course authoring page', () => {
).reply(404, {
response: { status: 404 },
});
await executeThunk(fetchCourseDetail(courseId), store.dispatch);
};
const mockStoreError = async () => {
axiosMock.onGet(
@@ -96,18 +82,10 @@ describe('Course authoring page', () => {
).reply(500, {
response: { status: 500 },
});
await executeThunk(fetchCourseDetail(courseId), store.dispatch);
};
test('renders not found page on non-existent course key', async () => {
await mockStoreNotFound();
const wrapper = render(
<AppProvider store={store}>
<IntlProvider locale="en">
<CourseAuthoringPage courseId={courseId} />
</IntlProvider>
</AppProvider>
,
);
const wrapper = renderComponent(<CourseAuthoringPage />);
expect(await wrapper.findByTestId('notFoundAlert')).toBeInTheDocument();
});
test('does not render not found page on other kinds of error', async () => {
@@ -117,17 +95,29 @@ describe('Course authoring page', () => {
// IN_PROGRESS but also not NOT_FOUND or DENIED- then check that the not
// found alert is not present.
const contentTestId = 'courseAuthoringPageContent';
const wrapper = render(
<AppProvider store={store}>
<IntlProvider locale="en">
<CourseAuthoringPage courseId={courseId}>
<div data-testid={contentTestId} />
</CourseAuthoringPage>
</IntlProvider>
</AppProvider>
const wrapper = renderComponent(
<CourseAuthoringPage>
<div data-testid={contentTestId} />
</CourseAuthoringPage>
,
);
expect(await wrapper.findByTestId(contentTestId)).toBeInTheDocument();
expect(wrapper.queryByTestId('notFoundAlert')).not.toBeInTheDocument();
});
const mockStoreDenied = async () => {
const studioApiBaseUrl = getConfig().STUDIO_BASE_URL;
const courseAppsApiUrl = `${studioApiBaseUrl}/api/course_apps/v1/apps`;
axiosMock.onGet(
`${courseAppsApiUrl}/${courseId}`,
).reply(403);
await executeThunk(fetchCourseApps(courseId), store.dispatch);
};
test('renders PermissionDeniedAlert when courseAppsApiStatus is DENIED', async () => {
mockPathname = '/editor/';
await mockStoreDenied();
const wrapper = renderComponent(<CourseAuthoringPage />);
expect(await wrapper.findByTestId('permissionDeniedAlert')).toBeInTheDocument();
});
});

View File

@@ -1,40 +1,36 @@
import React, { useEffect } from 'react';
import PropTypes from 'prop-types';
import { useDispatch, useSelector } from 'react-redux';
import {
useLocation,
} from 'react-router-dom';
import { StudioFooter } from '@edx/frontend-component-footer';
import { StudioFooterSlot } from '@edx/frontend-component-footer';
import Header from './header';
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 { fetchOnlyStudioHomeData } from './studio-home/data/thunks';
import { getCourseAppsApiStatus } from './pages-and-resources/data/selectors';
import { RequestStatus } from './data/constants';
import Loading from './generic/Loading';
import { useCourseAuthoringContext } from './CourseAuthoringContext';
const CourseAuthoringPage = ({ courseId, children }) => {
interface Props {
children?: React.ReactNode;
}
const CourseAuthoringPage = ({ children }: Props) => {
const dispatch = useDispatch();
useEffect(() => {
dispatch(fetchCourseDetail(courseId));
}, [courseId]);
useEffect(() => {
dispatch(fetchStudioHomeData());
dispatch(fetchOnlyStudioHomeData());
}, []);
const courseDetail = useModel('courseDetails', courseId);
const courseNumber = courseDetail ? courseDetail.number : null;
const courseOrg = courseDetail ? courseDetail.org : null;
const courseTitle = courseDetail ? courseDetail.name : courseId;
const { courseId, courseDetails, courseDetailStatus } = useCourseAuthoringContext();
const courseNumber = courseDetails?.number;
const courseOrg = courseDetails?.org;
const courseTitle = courseDetails?.name;
const inProgress = courseDetailStatus === RequestStatus.IN_PROGRESS || courseDetailStatus === RequestStatus.PENDING;
const courseAppsApiStatus = useSelector(getCourseAppsApiStatus);
const courseDetailStatus = useSelector(state => state.courseDetail.status);
const inProgress = courseDetailStatus === RequestStatus.IN_PROGRESS;
const { pathname } = useLocation();
const isEditor = pathname.includes('/editor');
@@ -61,22 +57,16 @@ const CourseAuthoringPage = ({ courseId, children }) => {
org={courseOrg}
title={courseTitle}
contextId={courseId}
containerProps={{
size: 'fluid',
}}
/>
)
)}
{children}
{!inProgress && !isEditor && <StudioFooter />}
{!inProgress && !isEditor && <StudioFooterSlot />}
</div>
);
};
CourseAuthoringPage.propTypes = {
children: PropTypes.node,
courseId: PropTypes.string.isRequired,
};
CourseAuthoringPage.defaultProps = {
children: null,
};
export default CourseAuthoringPage;

View File

@@ -1,138 +0,0 @@
import React from 'react';
import {
Navigate, Routes, Route, useParams,
} from 'react-router-dom';
import { getConfig } from '@edx/frontend-platform';
import { PageWrap } from '@edx/frontend-platform/react';
import { Textbooks } from 'CourseAuthoring/textbooks';
import CourseAuthoringPage from './CourseAuthoringPage';
import { PagesAndResources } from './pages-and-resources';
import EditorContainer from './editors/EditorContainer';
import VideoSelectorContainer from './selectors/VideoSelectorContainer';
import CustomPages from './custom-pages';
import { FilesPage, VideosPage } from './files-and-videos';
import { AdvancedSettings } from './advanced-settings';
import { CourseOutline } from './course-outline';
import ScheduleAndDetails from './schedule-and-details';
import { GradingSettings } from './grading-settings';
import CourseTeam from './course-team/CourseTeam';
import { CourseUpdates } from './course-updates';
import { CourseUnit } from './course-unit';
import { Certificates } from './certificates';
import CourseExportPage from './export-page/CourseExportPage';
import CourseImportPage from './import-page/CourseImportPage';
import { DECODED_ROUTES } from './constants';
import CourseChecklist from './course-checklist';
import GroupConfigurations from './group-configurations';
/**
* As of this writing, these routes are mounted at a path prefixed with the following:
*
* /course/:courseId
*
* Meaning that their absolute paths look like:
*
* /course/:courseId/course-pages
* /course/:courseId/proctored-exam-settings
* /course/:courseId/editor/:blockType/:blockId
*
* This component and CourseAuthoringPage should maybe be combined once we no longer need to have
* CourseAuthoringPage split out for use in LegacyProctoringRoute. Once that route is removed, we
* can move the Header/Footer rendering to this component and likely pull the course detail loading
* in as well, and it'd feel a bit better-factored and the roles would feel more clear.
*/
const CourseAuthoringRoutes = () => {
const { courseId } = useParams();
return (
<CourseAuthoringPage courseId={courseId}>
<Routes>
<Route
path="/"
element={<PageWrap><CourseOutline courseId={courseId} /></PageWrap>}
/>
<Route
path="course_info"
element={<PageWrap><CourseUpdates courseId={courseId} /></PageWrap>}
/>
<Route
path="assets"
element={<PageWrap><FilesPage courseId={courseId} /></PageWrap>}
/>
<Route
path="videos"
element={getConfig().ENABLE_VIDEO_UPLOAD_PAGE_LINK_IN_CONTENT_DROPDOWN === 'true' ? <PageWrap><VideosPage courseId={courseId} /></PageWrap> : null}
/>
<Route
path="pages-and-resources/*"
element={<PageWrap><PagesAndResources courseId={courseId} /></PageWrap>}
/>
<Route
path="proctored-exam-settings"
element={<Navigate replace to={`/course/${courseId}/pages-and-resources`} />}
/>
<Route
path="custom-pages/*"
element={<PageWrap><CustomPages courseId={courseId} /></PageWrap>}
/>
{DECODED_ROUTES.COURSE_UNIT.map((path) => (
<Route
key={path}
path={path}
element={<PageWrap><CourseUnit courseId={courseId} /></PageWrap>}
/>
))}
<Route
path="editor/course-videos/:blockId"
element={<PageWrap><VideoSelectorContainer courseId={courseId} /></PageWrap>}
/>
<Route
path="editor/:blockType/:blockId?"
element={<PageWrap><EditorContainer learningContextId={courseId} /></PageWrap>}
/>
<Route
path="settings/details"
element={<PageWrap><ScheduleAndDetails courseId={courseId} /></PageWrap>}
/>
<Route
path="settings/grading"
element={<PageWrap><GradingSettings courseId={courseId} /></PageWrap>}
/>
<Route
path="course_team"
element={<PageWrap><CourseTeam courseId={courseId} /></PageWrap>}
/>
<Route
path="group_configurations"
element={<PageWrap><GroupConfigurations courseId={courseId} /></PageWrap>}
/>
<Route
path="settings/advanced"
element={<PageWrap><AdvancedSettings courseId={courseId} /></PageWrap>}
/>
<Route
path="import"
element={<PageWrap><CourseImportPage courseId={courseId} /></PageWrap>}
/>
<Route
path="export"
element={<PageWrap><CourseExportPage courseId={courseId} /></PageWrap>}
/>
<Route
path="checklists"
element={<PageWrap><CourseChecklist courseId={courseId} /></PageWrap>}
/>
<Route
path="certificates"
element={getConfig().ENABLE_CERTIFICATE_PAGE === 'true' ? <PageWrap><Certificates courseId={courseId} /></PageWrap> : null}
/>
<Route
path="textbooks"
element={<PageWrap><Textbooks courseId={courseId} /></PageWrap>}
/>
</Routes>
</CourseAuthoringPage>
);
};
export default CourseAuthoringRoutes;

View File

@@ -1,117 +0,0 @@
import React from 'react';
import { AppProvider } from '@edx/frontend-platform/react';
import { initializeMockApp } from '@edx/frontend-platform';
import { render, screen } from '@testing-library/react';
import { MemoryRouter } from 'react-router-dom';
import CourseAuthoringRoutes from './CourseAuthoringRoutes';
import initializeStore from './store';
const courseId = 'course-v1:edX+TestX+Test_Course';
const pagesAndResourcesMockText = 'Pages And Resources';
const editorContainerMockText = 'Editor Container';
const videoSelectorContainerMockText = 'Video Selector Container';
const customPagesMockText = 'Custom Pages';
let store;
const mockComponentFn = jest.fn();
jest.mock('react-router-dom', () => ({
...jest.requireActual('react-router-dom'),
useParams: () => ({
courseId,
}),
}));
// 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,
setEditorRef: jest.fn().mockName('prepareEditorRef.setEditorRef'),
})),
}));
jest.mock('./pages-and-resources/PagesAndResources', () => (props) => {
mockComponentFn(props);
return pagesAndResourcesMockText;
});
jest.mock('./editors/EditorContainer', () => (props) => {
mockComponentFn(props);
return editorContainerMockText;
});
jest.mock('./selectors/VideoSelectorContainer', () => (props) => {
mockComponentFn(props);
return videoSelectorContainerMockText;
});
jest.mock('./custom-pages/CustomPages', () => (props) => {
mockComponentFn(props);
return customPagesMockText;
});
describe('<CourseAuthoringRoutes>', () => {
beforeEach(() => {
initializeMockApp({
authenticatedUser: {
userId: 3,
username: 'abc123',
administrator: true,
roles: [],
},
});
store = initializeStore();
});
fit('renders the PagesAndResources component when the pages and resources route is active', () => {
render(
<AppProvider store={store} wrapWithRouter={false}>
<MemoryRouter initialEntries={['/pages-and-resources']}>
<CourseAuthoringRoutes />
</MemoryRouter>
</AppProvider>,
);
expect(screen.getByText(pagesAndResourcesMockText)).toBeVisible();
expect(mockComponentFn).toHaveBeenCalledWith(
expect.objectContaining({
courseId,
}),
);
});
it('renders the EditorContainer component when the course editor route is active', () => {
render(
<AppProvider store={store} wrapWithRouter={false}>
<MemoryRouter initialEntries={['/editor/video/block-id']}>
<CourseAuthoringRoutes />
</MemoryRouter>
</AppProvider>,
);
expect(screen.queryByText(editorContainerMockText)).toBeInTheDocument();
expect(screen.queryByText(pagesAndResourcesMockText)).not.toBeInTheDocument();
expect(mockComponentFn).toHaveBeenCalledWith(
expect.objectContaining({
courseId,
}),
);
});
it('renders the VideoSelectorContainer component when the course videos route is active', () => {
render(
<AppProvider store={store} wrapWithRouter={false}>
<MemoryRouter initialEntries={['/editor/course-videos/block-id']}>
<CourseAuthoringRoutes />
</MemoryRouter>
</AppProvider>,
);
expect(screen.queryByText(videoSelectorContainerMockText)).toBeInTheDocument();
expect(screen.queryByText(pagesAndResourcesMockText)).not.toBeInTheDocument();
expect(mockComponentFn).toHaveBeenCalledWith(
expect.objectContaining({
courseId,
}),
);
});
});

View File

@@ -0,0 +1,99 @@
import CourseAuthoringRoutes from './CourseAuthoringRoutes';
import { getApiWaffleFlagsUrl } from './data/api';
import {
screen, initializeMocks, render, waitFor,
} from './testUtils';
const courseId = 'course-v1:edX+TestX+Test_Course';
const pagesAndResourcesMockText = 'Pages And Resources';
const editorContainerMockText = 'Editor Container';
const videoSelectorContainerMockText = 'Video Selector Container';
const customPagesMockText = 'Custom Pages';
const mockComponentFn = jest.fn();
jest.mock('react-router-dom', () => ({
...jest.requireActual('react-router-dom'),
useParams: () => ({
courseId,
}),
}));
// 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,
setEditorRef: jest.fn().mockName('prepareEditorRef.setEditorRef'),
})),
}));
jest.mock('./pages-and-resources/PagesAndResources', () => (props) => {
mockComponentFn(props);
return pagesAndResourcesMockText;
});
jest.mock('./editors/EditorContainer', () => (props) => {
mockComponentFn(props);
return editorContainerMockText;
});
jest.mock('./selectors/VideoSelectorContainer', () => (props) => {
mockComponentFn(props);
return videoSelectorContainerMockText;
});
jest.mock('./custom-pages/CustomPages', () => (props) => {
mockComponentFn(props);
return customPagesMockText;
});
describe('<CourseAuthoringRoutes>', () => {
beforeEach(async () => {
const user = {
userId: 1,
username: 'username',
};
const { axiosMock } = initializeMocks({ user });
axiosMock
.onGet(getApiWaffleFlagsUrl(courseId))
.reply(200, {});
});
it('renders the PagesAndResources component when the pages and resources route is active', async () => {
render(
<CourseAuthoringRoutes />,
{ routerProps: { initialEntries: ['/pages-and-resources'] } },
);
await waitFor(() => {
expect(screen.getByText(pagesAndResourcesMockText)).toBeVisible();
expect(mockComponentFn).toHaveBeenCalled();
});
});
it('renders the EditorContainer component when the course editor route is active', async () => {
render(
<CourseAuthoringRoutes />,
{ routerProps: { initialEntries: ['/editor/video/block-id'] } },
);
await waitFor(() => {
expect(screen.queryByText(editorContainerMockText)).toBeInTheDocument();
expect(screen.queryByText(pagesAndResourcesMockText)).not.toBeInTheDocument();
expect(mockComponentFn).toHaveBeenCalledWith(
expect.objectContaining({
learningContextId: courseId,
}),
);
});
});
it('renders the VideoSelectorContainer component when the course videos route is active', async () => {
render(
<CourseAuthoringRoutes />,
{ routerProps: { initialEntries: ['/editor/course-videos/block-id'] } },
);
await waitFor(() => {
expect(screen.queryByText(videoSelectorContainerMockText)).toBeInTheDocument();
expect(screen.queryByText(pagesAndResourcesMockText)).not.toBeInTheDocument();
expect(mockComponentFn).toHaveBeenCalled();
});
});
});

View File

@@ -0,0 +1,172 @@
import {
Navigate, Routes, Route, useParams,
} from 'react-router-dom';
import { getConfig } from '@edx/frontend-platform';
import { PageWrap } from '@edx/frontend-platform/react';
import { Textbooks } from './textbooks';
import CourseAuthoringPage from './CourseAuthoringPage';
import { PagesAndResources } from './pages-and-resources';
import EditorContainer from './editors/EditorContainer';
import VideoSelectorContainer from './selectors/VideoSelectorContainer';
import CustomPages from './custom-pages';
import { FilesPage, VideosPage } from './files-and-videos';
import { AdvancedSettings } from './advanced-settings';
import {
CourseOutline,
OutlineSidebarProvider,
OutlineSidebarPagesProvider,
} from './course-outline';
import ScheduleAndDetails from './schedule-and-details';
import { GradingSettings } from './grading-settings';
import CourseTeam from './course-team/CourseTeam';
import { CourseUpdates } from './course-updates';
import { CourseUnit, SubsectionUnitRedirect } from './course-unit';
import { Certificates } from './certificates';
import CourseExportPage from './export-page/CourseExportPage';
import CourseOptimizerPage from './optimizer-page/CourseOptimizerPage';
import CourseImportPage from './import-page/CourseImportPage';
import { DECODED_ROUTES } from './constants';
import CourseChecklist from './course-checklist';
import GroupConfigurations from './group-configurations';
import { CourseLibraries } from './course-libraries';
import { IframeProvider } from './generic/hooks/context/iFrameContext';
import { CourseAuthoringProvider } from './CourseAuthoringContext';
/**
* As of this writing, these routes are mounted at a path prefixed with the following:
*
* /course/:courseId
*
* Meaning that their absolute paths look like:
*
* /course/:courseId/course-pages
* /course/:courseId/proctored-exam-settings
* /course/:courseId/editor/:blockType/:blockId
*
* This component and CourseAuthoringPage should maybe be combined once we no longer need to have
* CourseAuthoringPage split out for use in LegacyProctoringRoute. Once that route is removed, we
* can move the Header/Footer rendering to this component and likely pull the course detail loading
* in as well, and it'd feel a bit better-factored and the roles would feel more clear.
*/
const CourseAuthoringRoutes = () => {
const { courseId } = useParams();
if (courseId === undefined) {
// istanbul ignore next - This shouldn't be possible; it's just here to satisfy the type checker.
throw new Error('Error: route is missing courseId.');
}
return (
<CourseAuthoringProvider courseId={courseId}>
<CourseAuthoringPage>
<Routes>
<Route
path="/"
element={(
<PageWrap>
<OutlineSidebarPagesProvider>
<OutlineSidebarProvider>
<CourseOutline />
</OutlineSidebarProvider>
</OutlineSidebarPagesProvider>
</PageWrap>
)}
/>
<Route
path="course_info"
element={<PageWrap><CourseUpdates /></PageWrap>}
/>
<Route
path="libraries"
element={<PageWrap><CourseLibraries /></PageWrap>}
/>
<Route
path="assets"
element={<PageWrap><FilesPage /></PageWrap>}
/>
<Route
path="videos"
element={getConfig().ENABLE_VIDEO_UPLOAD_PAGE_LINK_IN_CONTENT_DROPDOWN === 'true' ? <PageWrap><VideosPage /></PageWrap> : null}
/>
<Route
path="pages-and-resources/*"
element={<PageWrap><PagesAndResources /></PageWrap>}
/>
<Route
path="proctored-exam-settings"
element={<Navigate replace to={`/course/${courseId}/pages-and-resources`} />}
/>
<Route
path="custom-pages/*"
element={<PageWrap><CustomPages /></PageWrap>}
/>
<Route
path="/subsection/:subsectionId"
element={<PageWrap><SubsectionUnitRedirect /></PageWrap>}
/>
{DECODED_ROUTES.COURSE_UNIT.map((path) => (
<Route
key={path}
path={path}
element={<PageWrap><IframeProvider><CourseUnit /></IframeProvider></PageWrap>}
/>
))}
<Route
path="editor/course-videos/:blockId"
element={<PageWrap><VideoSelectorContainer /></PageWrap>}
/>
<Route
path="editor/:blockType/:blockId?"
element={<PageWrap><EditorContainer learningContextId={courseId} /></PageWrap>}
/>
<Route
path="settings/details"
element={<PageWrap><ScheduleAndDetails /></PageWrap>}
/>
<Route
path="settings/grading"
element={<PageWrap><GradingSettings /></PageWrap>}
/>
<Route
path="course_team"
element={<PageWrap><CourseTeam /></PageWrap>}
/>
<Route
path="group_configurations"
element={<PageWrap><GroupConfigurations /></PageWrap>}
/>
<Route
path="settings/advanced"
element={<PageWrap><AdvancedSettings /></PageWrap>}
/>
<Route
path="import"
element={<PageWrap><CourseImportPage /></PageWrap>}
/>
<Route
path="export"
element={<PageWrap><CourseExportPage /></PageWrap>}
/>
<Route
path="optimizer"
element={<PageWrap><CourseOptimizerPage /></PageWrap>}
/>
<Route
path="checklists"
element={<PageWrap><CourseChecklist /></PageWrap>}
/>
<Route
path="certificates"
element={getConfig().ENABLE_CERTIFICATE_PAGE === 'true' ? <PageWrap><Certificates /></PageWrap> : null}
/>
<Route
path="textbooks"
element={<PageWrap><Textbooks /></PageWrap>}
/>
</Routes>
</CourseAuthoringPage>
</CourseAuthoringProvider>
);
};
export default CourseAuthoringRoutes;

View File

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

View File

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

View File

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

4
src/__mocks__/index.ts Normal file
View File

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

View File

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

View File

@@ -0,0 +1,29 @@
import {
initializeMocks,
render,
screen,
} from '@src/testUtils';
import AccessibilityBody from './index';
const renderComponent = () => {
render(
<AccessibilityBody
communityAccessibilityLink="http://example.com"
email="example@example.com"
/>,
);
};
describe('<AccessibilityBody />', () => {
describe('renders', () => {
beforeEach(async () => {
initializeMocks();
});
it('contains links', () => {
renderComponent();
expect(screen.getAllByTestId('email-element')).toHaveLength(2);
expect(screen.getAllByTestId('accessibility-page-link')).toHaveLength(1);
});
});
});

View File

@@ -1,6 +1,4 @@
import React from 'react';
import PropTypes from 'prop-types';
import { injectIntl, FormattedMessage } from '@edx/frontend-platform/i18n';
import { FormattedMessage } from '@edx/frontend-platform/i18n';
import { Hyperlink, MailtoLink, Stack } from '@openedx/paragon';
import messages from './messages';
@@ -8,6 +6,9 @@ import messages from './messages';
const AccessibilityBody = ({
communityAccessibilityLink,
email,
}: {
communityAccessibilityLink: string,
email: string,
}) => (
<div className="mt-5">
<header>
@@ -90,9 +91,4 @@ const AccessibilityBody = ({
</div>
);
AccessibilityBody.propTypes = {
communityAccessibilityLink: PropTypes.string.isRequired,
email: PropTypes.string.isRequired,
};
export default injectIntl(AccessibilityBody);
export default AccessibilityBody;

View File

@@ -1,57 +1,31 @@
import {
initializeMocks,
render,
act,
screen,
} from '@testing-library/react';
} from '@src/testUtils';
import userEvent from '@testing-library/user-event';
import { initializeMockApp } from '@edx/frontend-platform';
import MockAdapter from 'axios-mock-adapter';
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
import { AppProvider } from '@edx/frontend-platform/react';
import { IntlProvider } from '@edx/frontend-platform/i18n';
import initializeStore from '../../store';
import { RequestStatus } from '../../data/constants';
import AccessibilityForm from './index';
import { getZendeskrUrl } from '../data/api';
import messages from './messages';
let axiosMock;
let store;
const defaultProps = {
accessibilityEmail: 'accessibilityTest@test.com',
};
const initialState = {
accessibilityPage: {
savingStatus: '',
},
};
const renderComponent = () => {
render(
<IntlProvider locale="en">
<AppProvider store={store}>
<AccessibilityForm {...defaultProps} />
</AppProvider>
</IntlProvider>,
<AccessibilityForm {...defaultProps} />,
);
};
describe('<AccessibilityPolicyForm />', () => {
beforeEach(async () => {
initializeMockApp({
authenticatedUser: {
userId: 3,
username: 'abc123',
administrator: false,
roles: [],
},
});
store = initializeStore(initialState);
axiosMock = new MockAdapter(getAuthenticatedHttpClient());
const mocks = initializeMocks();
axiosMock = mocks.axiosMock;
});
describe('renders', () => {
@@ -74,24 +48,35 @@ describe('<AccessibilityPolicyForm />', () => {
describe('statusAlert', () => {
let formSections;
let submitButton;
let user;
beforeEach(async () => {
user = userEvent.setup();
renderComponent();
formSections = screen.getAllByRole('textbox');
await act(async () => {
userEvent.type(formSections[0], 'email@email.com');
userEvent.type(formSections[1], 'test name');
userEvent.type(formSections[2], 'feedback message');
});
await user.type(formSections[0], 'email@email.com');
await user.type(formSections[1], 'test name');
await user.type(formSections[2], 'feedback message');
submitButton = screen.getByText(messages.accessibilityPolicyFormSubmitLabel.defaultMessage);
});
it('renders in progress state', async () => {
axiosMock.onPost(getZendeskrUrl()).reply(
() => new Promise(() => {
// always in pending
}),
);
await user.click(submitButton);
expect(screen.getByRole('button', { name: /submitting/i })).toBeInTheDocument();
});
it('shows correct success message', async () => {
axiosMock.onPost(getZendeskrUrl()).reply(200);
await act(async () => {
userEvent.click(submitButton);
});
const { savingStatus } = store.getState().accessibilityPage;
expect(savingStatus).toEqual(RequestStatus.SUCCESSFUL);
await user.click(submitButton);
expect(screen.getAllByRole('alert')).toHaveLength(1);
@@ -104,11 +89,8 @@ describe('<AccessibilityPolicyForm />', () => {
it('shows correct rate limiting message', async () => {
axiosMock.onPost(getZendeskrUrl()).reply(429);
await act(async () => {
userEvent.click(submitButton);
});
const { savingStatus } = store.getState().accessibilityPage;
expect(savingStatus).toEqual(RequestStatus.FAILED);
await user.click(submitButton);
expect(screen.getAllByRole('alert')).toHaveLength(1);
@@ -123,23 +105,24 @@ describe('<AccessibilityPolicyForm />', () => {
describe('input validation', () => {
let formSections;
let submitButton;
let user;
beforeEach(async () => {
user = userEvent.setup();
renderComponent();
formSections = screen.getAllByRole('textbox');
await act(async () => {
userEvent.type(formSections[0], 'email@email.com');
userEvent.type(formSections[1], 'test name');
userEvent.type(formSections[2], 'feedback message');
});
await user.type(formSections[0], 'email@email.com');
await user.type(formSections[1], 'test name');
await user.type(formSections[2], 'feedback message');
submitButton = screen.getByText(messages.accessibilityPolicyFormSubmitLabel.defaultMessage);
});
it('adds validation checking on each input field', async () => {
await act(async () => {
userEvent.clear(formSections[0]);
userEvent.clear(formSections[1]);
userEvent.clear(formSections[2]);
});
await user.clear(formSections[0]);
await user.clear(formSections[1]);
await user.clear(formSections[2]);
const emailError = screen.getByTestId('error-feedback-email');
expect(emailError).toBeVisible();
@@ -151,12 +134,10 @@ describe('<AccessibilityPolicyForm />', () => {
});
it('sumbit button is disabled when trying to submit with all empty fields', async () => {
await act(async () => {
userEvent.clear(formSections[0]);
userEvent.clear(formSections[1]);
userEvent.clear(formSections[2]);
userEvent.click(submitButton);
});
await user.clear(formSections[0]);
await user.clear(formSections[1]);
await user.clear(formSections[2]);
await user.click(submitButton);
expect(submitButton.closest('button')).toBeDisabled();
});

View File

@@ -1,33 +1,26 @@
import React from 'react';
import PropTypes from 'prop-types';
import {
injectIntl, FormattedMessage, intlShape, FormattedDate, FormattedTime,
FormattedMessage, FormattedDate, FormattedTime, useIntl,
} from '@edx/frontend-platform/i18n';
import {
ActionRow, Alert, Form, Stack, StatefulButton,
} from '@openedx/paragon';
import { RequestStatus } from '../../data/constants';
import { STATEFUL_BUTTON_STATES } from '../../constants';
import submitAccessibilityForm from '../data/thunks';
import { STATEFUL_BUTTON_STATES } from '@src/constants';
import useAccessibility from './hooks';
import messages from './messages';
const AccessibilityForm = ({
accessibilityEmail,
// injected
intl,
}) => {
const AccessibilityForm = ({ accessibilityEmail }: { accessibilityEmail: string }) => {
const intl = useIntl();
const {
errors,
values,
isFormFilled,
dispatch,
mutation,
handleBlur,
handleChange,
hasErrorField,
savingStatus,
} = useAccessibility({ name: '', email: '', message: '' }, intl);
} = useAccessibility({ name: '', email: '', message: '' });
const formFields = [
{
@@ -56,7 +49,7 @@ const AccessibilityForm = ({
};
const handleSubmit = () => {
dispatch(submitAccessibilityForm(values));
mutation.mutateAsync(values).catch(() => {});
};
const start = new Date('Mon Jan 29 2018 13:00:00 GMT (UTC)');
@@ -67,7 +60,7 @@ const AccessibilityForm = ({
<h2 className="my-4">
<FormattedMessage {...messages.accessibilityPolicyFormHeader} />
</h2>
{savingStatus === RequestStatus.SUCCESSFUL && (
{savingStatus === 'success' && (
<Alert variant="success">
<Stack gap={2}>
<div className="mb-2">
@@ -87,7 +80,7 @@ const AccessibilityForm = ({
</Stack>
</Alert>
)}
{savingStatus === RequestStatus.FAILED && (
{savingStatus === 'error' && (
<Alert variant="danger">
<div data-testid="rate-limit-alert">
<FormattedMessage
@@ -126,7 +119,7 @@ const AccessibilityForm = ({
onClick={handleSubmit}
disabled={!isFormFilled}
state={
savingStatus === RequestStatus.IN_PROGRESS
savingStatus === 'pending'
? STATEFUL_BUTTON_STATES.pending
: STATEFUL_BUTTON_STATES.default
}
@@ -137,10 +130,4 @@ const AccessibilityForm = ({
);
};
AccessibilityForm.propTypes = {
accessibilityEmail: PropTypes.string.isRequired,
// injected
intl: intlShape.isRequired,
};
export default injectIntl(AccessibilityForm);
export default AccessibilityForm;

View File

@@ -1,14 +1,14 @@
import { useEffect, useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { useIntl } from '@edx/frontend-platform/i18n';
import { useFormik } from 'formik';
import * as Yup from 'yup';
import { RequestStatus } from '../../data/constants';
import messages from './messages';
import { useSubmitAccessibilityForm } from '../data/apiHooks';
import { AccessibilityFormData } from '../data/api';
const useAccessibility = (initialValues, intl) => {
const dispatch = useDispatch();
const savingStatus = useSelector(state => state.accessibilityPage.savingStatus);
const useAccessibility = (initialValues: AccessibilityFormData) => {
const intl = useIntl();
const [isFormFilled, setFormFilled] = useState(false);
const validationSchema = Yup.object().shape({
name: Yup.string().required(
@@ -29,29 +29,27 @@ const useAccessibility = (initialValues, intl) => {
enableReinitialize: true,
validateOnBlur: false,
validationSchema,
/* istanbul ignore next */
onSubmit: () => {},
});
const mutation = useSubmitAccessibilityForm(handleReset);
useEffect(() => {
setFormFilled(Object.values(values).every((i) => i));
}, [values]);
useEffect(() => {
if (savingStatus === RequestStatus.SUCCESSFUL) {
handleReset();
}
}, [savingStatus]);
const hasErrorField = (fieldName) => !!errors[fieldName] && !!touched[fieldName];
return {
errors,
values,
isFormFilled,
dispatch,
mutation,
handleBlur,
handleChange,
hasErrorField,
savingStatus,
savingStatus: mutation.status,
};
};

View File

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

View File

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

View File

@@ -0,0 +1,16 @@
import { initializeMocks, render, screen } from '../testUtils';
import AccessibilityPage from './index';
const renderComponent = () => render(<AccessibilityPage />);
describe('<AccessibilityPolicyPage />', () => {
describe('renders', () => {
beforeEach(async () => {
initializeMocks();
});
it('contains the policy body', () => {
renderComponent();
expect(screen.getByText('Individualized Accessibility Process for Course Creators')).toBeVisible();
});
});
});

View File

@@ -0,0 +1,40 @@
import { useIntl } from '@edx/frontend-platform/i18n';
import { getExternalLinkUrl } from '@edx/frontend-platform';
import { Helmet } from 'react-helmet';
import { Container } from '@openedx/paragon';
import { StudioFooterSlot } from '@edx/frontend-component-footer';
import Header from '../header';
import messages from './messages';
import AccessibilityBody from './AccessibilityBody';
import AccessibilityForm from './AccessibilityForm';
import { COMMUNITY_ACCESSIBILITY_LINK, ACCESSIBILITY_EMAIL } from './constants';
const AccessibilityPage = () => {
const intl = useIntl();
return (
<>
<Helmet>
<title>
{intl.formatMessage(messages.pageTitle, {
siteName: process.env.SITE_NAME,
})}
</title>
</Helmet>
<Header isHiddenMainMenu />
<Container size="xl" className="px-4">
<AccessibilityBody
{...{
email: ACCESSIBILITY_EMAIL,
communityAccessibilityLink: getExternalLinkUrl(COMMUNITY_ACCESSIBILITY_LINK),
}}
/>
<AccessibilityForm accessibilityEmail={ACCESSIBILITY_EMAIL} />
</Container>
<StudioFooterSlot />
</>
);
};
export default AccessibilityPage;

View File

@@ -0,0 +1,2 @@
export const COMMUNITY_ACCESSIBILITY_LINK = 'https://www.edx.org/accessibility';
export const ACCESSIBILITY_EMAIL = 'accessibility@edx.org';

View File

@@ -8,12 +8,20 @@ ensureConfig([
export const getApiBaseUrl = () => getConfig().STUDIO_BASE_URL;
export const getZendeskrUrl = () => `${getApiBaseUrl()}/zendesk_proxy/v0`;
export interface AccessibilityFormData {
name: string;
email: string;
message: string;
}
/**
* Posts the form data to zendesk endpoint
* @param {string} courseId
* @returns {Promise<[{}]>}
*/
export async function postAccessibilityForm({ name, email, message }) {
export async function postAccessibilityForm({
name,
email,
message,
}: AccessibilityFormData) {
const data = {
name,
tags: ['studio_a11y'],

View File

@@ -0,0 +1,12 @@
import { useMutation } from '@tanstack/react-query';
import { AccessibilityFormData, postAccessibilityForm } from './api';
/**
* Mutation to submit accessibility form
*/
export const useSubmitAccessibilityForm = (handleSuccess: (e: any) => void) => (
useMutation({
mutationFn: (formData: AccessibilityFormData) => postAccessibilityForm(formData),
onSuccess: handleSuccess,
})
);

View File

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

View File

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

View File

@@ -1,15 +1,14 @@
import React, { useEffect, useState } from 'react';
import PropTypes from 'prop-types';
import { useEffect, useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import {
Container, Button, Layout, StatefulButton, TransitionReplace,
} from '@openedx/paragon';
import { CheckCircle, Info, Warning } from '@openedx/paragon/icons';
import { FormattedMessage, injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { FormattedMessage, useIntl } from '@edx/frontend-platform/i18n';
import { useCourseAuthoringContext } from '@src/CourseAuthoringContext';
import Placeholder from '../editors/Placeholder';
import AlertProctoringError from '../generic/AlertProctoringError';
import { useModel } from '../generic/model-store';
import InternetConnectionAlert from '../generic/internet-connection-alert';
import { parseArrayOrObjectValues } from '../utils';
import { RequestStatus } from '../data/constants';
@@ -26,7 +25,8 @@ import messages from './messages';
import ModalError from './modal-error/ModalError';
import getPageHeadTitle from '../generic/utils';
const AdvancedSettings = ({ intl, courseId }) => {
const AdvancedSettings = () => {
const intl = useIntl();
const dispatch = useDispatch();
const [saveSettingsPrompt, showSaveSettingsPrompt] = useState(false);
const [showDeprecated, setShowDeprecated] = useState(false);
@@ -38,7 +38,7 @@ const AdvancedSettings = ({ intl, courseId }) => {
const [isEditableState, setIsEditableState] = useState(false);
const [hasInternetConnectionError, setInternetConnectionError] = useState(false);
const courseDetails = useModel('courseDetails', courseId);
const { courseId, courseDetails } = useCourseAuthoringContext();
document.title = getPageHeadTitle(courseDetails?.name, intl.formatMessage(messages.headingTitle));
useEffect(() => {
@@ -277,9 +277,4 @@ const AdvancedSettings = ({ intl, courseId }) => {
);
};
AdvancedSettings.propTypes = {
intl: intlShape.isRequired,
courseId: PropTypes.string.isRequired,
};
export default injectIntl(AdvancedSettings);
export default AdvancedSettings;

View File

@@ -1,12 +1,10 @@
import React from 'react';
import { initializeMockApp } from '@edx/frontend-platform';
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
import { IntlProvider, injectIntl } from '@edx/frontend-platform/i18n';
import { AppProvider } from '@edx/frontend-platform/react';
import { render, fireEvent, waitFor } from '@testing-library/react';
import MockAdapter from 'axios-mock-adapter';
import initializeStore from '../store';
import { CourseAuthoringProvider } from '@src/CourseAuthoringContext';
import {
render as baseRender,
fireEvent,
initializeMocks,
waitFor,
} from '../testUtils';
import { executeThunk } from '../utils';
import { advancedSettingsMock } from './__mocks__';
import { getCourseAdvancedSettingsApiUrl } from './data/api';
@@ -28,39 +26,24 @@ jest.mock('react-textarea-autosize', () => jest.fn((props) => (
/>
)));
jest.mock('react-router-dom', () => ({
...jest.requireActual('react-router-dom'),
useLocation: () => ({
pathname: mockPathname,
}),
}));
const RootWrapper = () => (
<AppProvider store={store}>
<IntlProvider locale="en" messages={{}}>
<AdvancedSettings intl={injectIntl} courseId={courseId} />
</IntlProvider>
</AppProvider>
const render = () => baseRender(
<CourseAuthoringProvider courseId={courseId}>
<AdvancedSettings />
</CourseAuthoringProvider>,
{ path: mockPathname },
);
describe('<AdvancedSettings />', () => {
beforeEach(() => {
initializeMockApp({
authenticatedUser: {
userId: 3,
username: 'abc123',
administrator: true,
roles: [],
},
});
store = initializeStore();
axiosMock = new MockAdapter(getAuthenticatedHttpClient());
const mocks = initializeMocks();
store = mocks.reduxStore;
axiosMock = mocks.axiosMock;
axiosMock
.onGet(`${getCourseAdvancedSettingsApiUrl(courseId)}?fetch_all=0`)
.reply(200, advancedSettingsMock);
});
it('should render without errors', async () => {
const { getByText } = render(<RootWrapper />);
const { getByText } = render();
await waitFor(() => {
expect(getByText(messages.headingSubtitle.defaultMessage)).toBeInTheDocument();
const advancedSettingsElement = getByText(messages.headingTitle.defaultMessage, {
@@ -72,7 +55,7 @@ describe('<AdvancedSettings />', () => {
});
});
it('should render setting element', async () => {
const { getByText, queryByText } = render(<RootWrapper />);
const { getByText, queryByText } = render();
await waitFor(() => {
const advancedModuleListTitle = getByText(/Advanced Module List/i);
expect(advancedModuleListTitle).toBeInTheDocument();
@@ -80,7 +63,7 @@ describe('<AdvancedSettings />', () => {
});
});
it('should change to onСhange', async () => {
const { getByLabelText } = render(<RootWrapper />);
const { getByLabelText } = render();
await waitFor(() => {
const textarea = getByLabelText(/Advanced Module List/i);
expect(textarea).toBeInTheDocument();
@@ -89,7 +72,7 @@ describe('<AdvancedSettings />', () => {
});
});
it('should display a warning alert', async () => {
const { getByLabelText, getByText } = render(<RootWrapper />);
const { getByLabelText, getByText } = render();
await waitFor(() => {
const textarea = getByLabelText(/Advanced Module List/i);
fireEvent.change(textarea, { target: { value: '[3, 2, 1]' } });
@@ -100,7 +83,7 @@ describe('<AdvancedSettings />', () => {
});
});
it('should display a tooltip on clicking on the icon', async () => {
const { getByLabelText, getByText } = render(<RootWrapper />);
const { getByLabelText, getByText } = render();
await waitFor(() => {
const button = getByLabelText(/Show help text/i);
fireEvent.click(button);
@@ -108,7 +91,7 @@ describe('<AdvancedSettings />', () => {
});
});
it('should change deprecated button text ', async () => {
const { getByText } = render(<RootWrapper />);
const { getByText } = render();
await waitFor(() => {
const showDeprecatedItemsBtn = getByText(/Show Deprecated Settings/i);
expect(showDeprecatedItemsBtn).toBeInTheDocument();
@@ -118,7 +101,7 @@ describe('<AdvancedSettings />', () => {
expect(getByText('Certificate web/html view enabled')).toBeInTheDocument();
});
it('should reset to default value on click on Cancel button', async () => {
const { getByLabelText, getByText } = render(<RootWrapper />);
const { getByLabelText, getByText } = render();
let textarea;
await waitFor(() => {
textarea = getByLabelText(/Advanced Module List/i);
@@ -129,7 +112,7 @@ describe('<AdvancedSettings />', () => {
expect(textarea.value).toBe('[]');
});
it('should update the textarea value and display the updated value after clicking "Change manually"', async () => {
const { getByLabelText, getByText } = render(<RootWrapper />);
const { getByLabelText, getByText } = render();
let textarea;
await waitFor(() => {
textarea = getByLabelText(/Advanced Module List/i);
@@ -141,7 +124,7 @@ describe('<AdvancedSettings />', () => {
expect(textarea.value).toBe('[3, 2, 1,');
});
it('should show success alert after save', async () => {
const { getByLabelText, getByText } = render(<RootWrapper />);
const { getByLabelText, getByText } = render();
let textarea;
await waitFor(() => {
textarea = getByLabelText(/Advanced Module List/i);

View File

@@ -1,4 +1,4 @@
module.exports = {
export default {
advancedModules: {
deprecated: false,
displayName: 'Advanced Module List',

View File

@@ -1,2 +1 @@
// eslint-disable-next-line import/prefer-default-export
export { default as advancedSettingsMock } from './advancedSettings';

View File

@@ -1,6 +1,10 @@
/* eslint-disable import/prefer-default-export */
import { camelCaseObject, getConfig } from '@edx/frontend-platform';
import {
camelCaseObject,
getConfig,
} from '@edx/frontend-platform';
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
import { camelCase } from 'lodash';
import { convertObjectToSnakeCase } from '../../utils';
const getApiBaseUrl = () => getConfig().STUDIO_BASE_URL;
@@ -15,7 +19,19 @@ const getProctoringErrorsApiUrl = () => `${getApiBaseUrl()}/api/contentstore/v1/
export async function getCourseAdvancedSettings(courseId) {
const { data } = await getAuthenticatedHttpClient()
.get(`${getCourseAdvancedSettingsApiUrl(courseId)}?fetch_all=0`);
return camelCaseObject(data);
const keepValues = {};
Object.keys(data).forEach((key) => {
keepValues[camelCase(key)] = { value: data[key].value };
});
const formattedData = {};
const formattedCamelCaseData = camelCaseObject(data);
Object.keys(formattedCamelCaseData).forEach((key) => {
formattedData[key] = {
...formattedCamelCaseData[key],
value: keepValues[key]?.value,
};
});
return formattedData;
}
/**
@@ -27,7 +43,19 @@ export async function getCourseAdvancedSettings(courseId) {
export async function updateCourseAdvancedSettings(courseId, settings) {
const { data } = await getAuthenticatedHttpClient()
.patch(`${getCourseAdvancedSettingsApiUrl(courseId)}`, convertObjectToSnakeCase(settings));
return camelCaseObject(data);
const keepValues = {};
Object.keys(data).forEach((key) => {
keepValues[camelCase(key)] = { value: data[key].value };
});
const formattedData = {};
const formattedCamelCaseData = camelCaseObject(data);
Object.keys(formattedCamelCaseData).forEach((key) => {
formattedData[key] = {
...formattedCamelCaseData[key],
value: keepValues[key]?.value,
};
});
return formattedData;
}
/**
@@ -37,5 +65,17 @@ export async function updateCourseAdvancedSettings(courseId, settings) {
*/
export async function getProctoringExamErrors(courseId) {
const { data } = await getAuthenticatedHttpClient().get(`${getProctoringErrorsApiUrl()}${courseId}`);
return camelCaseObject(data);
const keepValues = {};
Object.keys(data).forEach((key) => {
keepValues[camelCase(key)] = { value: data[key].value };
});
const formattedData = {};
const formattedCamelCaseData = camelCaseObject(data);
Object.keys(formattedCamelCaseData).forEach((key) => {
formattedData[key] = {
...formattedCamelCaseData[key],
value: keepValues[key]?.value,
};
});
return formattedData;
}

View File

@@ -0,0 +1,236 @@
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
import {
getCourseAdvancedSettings,
updateCourseAdvancedSettings,
getProctoringExamErrors,
} from './api';
jest.mock('@edx/frontend-platform/auth', () => ({
getAuthenticatedHttpClient: jest.fn(),
}));
describe('courseSettings API', () => {
const mockHttpClient = {
get: jest.fn(),
patch: jest.fn(),
};
beforeEach(() => {
jest.clearAllMocks();
getAuthenticatedHttpClient.mockReturnValue(mockHttpClient);
});
describe('getCourseAdvancedSettings', () => {
it('should fetch and unformat course advanced settings', async () => {
const fakeData = {
key_snake_case: {
display_name: 'To come camelCase',
testCamelCase: 'This key must not be formatted',
PascalCase: 'To come camelCase',
'kebab-case': 'To come camelCase',
UPPER_CASE: 'To come camelCase',
lowercase: 'This key must not be formatted',
UPPERCASE: 'To come lowercase',
'Title Case': 'To come camelCase',
'dot.case': 'To come camelCase',
SCREAMING_SNAKE_CASE: 'To come camelCase',
MixedCase: 'To come camelCase',
'Train-Case': 'To come camelCase',
nestedOption: {
anotherOption: 'To come camelCase',
},
// value is an object with various cases
// this contain must not be formatted to camelCase
value: {
snake_case: 'snake_case',
camelCase: 'camelCase',
PascalCase: 'PascalCase',
'kebab-case': 'kebab-case',
UPPER_CASE: 'UPPER_CASE',
lowercase: 'lowercase',
UPPERCASE: 'UPPERCASE',
'Title Case': 'Title Case',
'dot.case': 'dot.case',
SCREAMING_SNAKE_CASE: 'SCREAMING_SNAKE_CASE',
MixedCase: 'MixedCase',
'Train-Case': 'Train-Case',
nestedOption: {
anotherOption: 'nestedContent',
},
},
},
};
const expected = {
keySnakeCase: {
displayName: 'To come camelCase',
testCamelCase: 'This key must not be formatted',
pascalCase: 'To come camelCase',
kebabCase: 'To come camelCase',
upperCase: 'To come camelCase',
lowercase: 'This key must not be formatted',
uppercase: 'To come lowercase',
titleCase: 'To come camelCase',
dotCase: 'To come camelCase',
screamingSnakeCase: 'To come camelCase',
mixedCase: 'To come camelCase',
trainCase: 'To come camelCase',
nestedOption: {
anotherOption: 'To come camelCase',
},
value: fakeData.key_snake_case.value,
},
};
mockHttpClient.get.mockResolvedValue({ data: fakeData });
const result = await getCourseAdvancedSettings('course-v1:Test+T101+2024');
expect(mockHttpClient.get).toHaveBeenCalledWith(
`${process.env.STUDIO_BASE_URL}/api/contentstore/v0/advanced_settings/course-v1:Test+T101+2024?fetch_all=0`,
);
expect(result).toEqual(expected);
});
});
describe('updateCourseAdvancedSettings', () => {
it('should update and unformat course advanced settings', async () => {
const fakeData = {
key_snake_case: {
display_name: 'To come camelCase',
testCamelCase: 'This key must not be formatted', // because already be camelCase
PascalCase: 'To come camelCase',
'kebab-case': 'To come camelCase',
UPPER_CASE: 'To come camelCase',
lowercase: 'This key must not be formatted', // because camelCase in lowercase not formatted
UPPERCASE: 'To come lowercase', // because camelCase in UPPERCASE format to lowercase
'Title Case': 'To come camelCase',
'dot.case': 'To come camelCase',
SCREAMING_SNAKE_CASE: 'To come camelCase',
MixedCase: 'To come camelCase',
'Train-Case': 'To come camelCase',
nestedOption: {
anotherOption: 'To come camelCase',
},
// value is an object with various cases
// this contain must not be formatted to camelCase
value: {
snake_case: 'snake_case',
camelCase: 'camelCase',
PascalCase: 'PascalCase',
'kebab-case': 'kebab-case',
UPPER_CASE: 'UPPER_CASE',
lowercase: 'lowercase',
UPPERCASE: 'UPPERCASE',
'Title Case': 'Title Case',
'dot.case': 'dot.case',
SCREAMING_SNAKE_CASE: 'SCREAMING_SNAKE_CASE',
MixedCase: 'MixedCase',
'Train-Case': 'Train-Case',
nestedOption: {
anotherOption: 'nestedContent',
},
},
},
};
const expected = {
keySnakeCase: {
displayName: 'To come camelCase',
testCamelCase: 'This key must not be formatted',
pascalCase: 'To come camelCase',
kebabCase: 'To come camelCase',
upperCase: 'To come camelCase',
lowercase: 'This key must not be formatted',
uppercase: 'To come lowercase',
titleCase: 'To come camelCase',
dotCase: 'To come camelCase',
screamingSnakeCase: 'To come camelCase',
mixedCase: 'To come camelCase',
trainCase: 'To come camelCase',
nestedOption: {
anotherOption: 'To come camelCase',
},
value: fakeData.key_snake_case.value,
},
};
mockHttpClient.patch.mockResolvedValue({ data: fakeData });
const result = await updateCourseAdvancedSettings('course-v1:Test+T101+2024', {});
expect(mockHttpClient.patch).toHaveBeenCalledWith(
`${process.env.STUDIO_BASE_URL}/api/contentstore/v0/advanced_settings/course-v1:Test+T101+2024`,
{},
);
expect(result).toEqual(expected);
});
});
describe('getProctoringExamErrors', () => {
it('should fetch proctoring errors and return unformat object', async () => {
const fakeData = {
key_snake_case: {
display_name: 'To come camelCase',
testCamelCase: 'This key must not be formatted',
PascalCase: 'To come camelCase',
'kebab-case': 'To come camelCase',
UPPER_CASE: 'To come camelCase',
lowercase: 'This key must not be formatted',
UPPERCASE: 'To come lowercase',
'Title Case': 'To come camelCase',
'dot.case': 'To come camelCase',
SCREAMING_SNAKE_CASE: 'To come camelCase',
MixedCase: 'To come camelCase',
'Train-Case': 'To come camelCase',
nestedOption: {
anotherOption: 'To come camelCase',
},
// value is an object with various cases
// this contain must not be formatted to camelCase
value: {
snake_case: 'snake_case',
camelCase: 'camelCase',
PascalCase: 'PascalCase',
'kebab-case': 'kebab-case',
UPPER_CASE: 'UPPER_CASE',
lowercase: 'lowercase',
UPPERCASE: 'UPPERCASE',
'Title Case': 'Title Case',
'dot.case': 'dot.case',
SCREAMING_SNAKE_CASE: 'SCREAMING_SNAKE_CASE',
MixedCase: 'MixedCase',
'Train-Case': 'Train-Case',
nestedOption: {
anotherOption: 'nestedContent',
},
},
},
};
const expected = {
keySnakeCase: {
displayName: 'To come camelCase',
testCamelCase: 'This key must not be formatted',
pascalCase: 'To come camelCase',
kebabCase: 'To come camelCase',
upperCase: 'To come camelCase',
lowercase: 'This key must not be formatted',
uppercase: 'To come lowercase',
titleCase: 'To come camelCase',
dotCase: 'To come camelCase',
screamingSnakeCase: 'To come camelCase',
mixedCase: 'To come camelCase',
trainCase: 'To come camelCase',
nestedOption: {
anotherOption: 'To come camelCase',
},
value: fakeData.key_snake_case.value,
},
};
mockHttpClient.get.mockResolvedValue({ data: fakeData });
const result = await getProctoringExamErrors('course-v1:Test+T101+2024');
expect(mockHttpClient.get).toHaveBeenCalledWith(
`${process.env.STUDIO_BASE_URL}/api/contentstore/v1/proctoring_errors/course-v1:Test+T101+2024`,
);
expect(result).toEqual(expected);
});
});
});

View File

@@ -25,7 +25,7 @@ export function fetchCourseAppSettings(courseId) {
sortedDisplayName.push(displayName);
});
const sortedSettingValues = {};
sortedDisplayName.sort().forEach((displayName => {
sortedDisplayName.sort((a, b) => a.localeCompare(b)).forEach((displayName => {
Object.entries(settingValues).forEach(([key, value]) => {
if (value.displayName === displayName) {
sortedSettingValues[key] = value;
@@ -58,7 +58,7 @@ export function updateCourseAppSetting(courseId, settings) {
try {
const { customAttributes: { httpErrorResponseData } } = error;
errorData = JSON.parse(httpErrorResponseData);
} catch (err) {
} catch {
errorData = {};
}
@@ -77,7 +77,7 @@ export function fetchProctoringExamErrors(courseId) {
const settingValues = await getProctoringExamErrors(courseId);
dispatch(fetchProctoringExamErrorsSuccess(settingValues));
return true;
} catch (error) {
} catch {
dispatch(updateSavingStatus({ status: RequestStatus.FAILED }));
return false;
}

View File

@@ -1,2 +1 @@
/* eslint-disable import/prefer-default-export */
export { default as AdvancedSettings } from './AdvancedSettings';

View File

@@ -1,55 +1,57 @@
import React from 'react';
import PropTypes from 'prop-types';
import { ActionRow, AlertModal, Button } from '@openedx/paragon';
import { FormattedMessage, injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { FormattedMessage, useIntl } from '@edx/frontend-platform/i18n';
import ModalErrorListItem from './ModalErrorListItem';
import messages from './messages';
const ModalError = ({
intl, isError, handleUndoChanges, showErrorModal, errorList, settingsData,
}) => (
<AlertModal
title={intl.formatMessage(messages.modalErrorTitle)}
isOpen={isError}
variant="danger"
footerNode={(
<ActionRow>
<Button
variant="tertiary"
onClick={() => showErrorModal(!isError)}
>
{intl.formatMessage(messages.modalErrorButtonChangeManually)}
</Button>
<Button onClick={handleUndoChanges}>
{intl.formatMessage(messages.modalErrorButtonUndoChanges)}
</Button>
</ActionRow>
isError, handleUndoChanges, showErrorModal, errorList, settingsData,
}) => {
const intl = useIntl();
return (
<AlertModal
title={intl.formatMessage(messages.modalErrorTitle)}
isOpen={isError}
variant="danger"
footerNode={(
<ActionRow>
<Button
variant="tertiary"
onClick={() => showErrorModal(!isError)}
>
{intl.formatMessage(messages.modalErrorButtonChangeManually)}
</Button>
<Button onClick={handleUndoChanges}>
{intl.formatMessage(messages.modalErrorButtonUndoChanges)}
</Button>
</ActionRow>
)}
>
<p>
<FormattedMessage
id="course-authoring.advanced-settings.modal.error.description"
defaultMessage="There was {errorCounter} while trying to save the course settings in the database.
>
<p>
<FormattedMessage
id="course-authoring.advanced-settings.modal.error.description"
defaultMessage="There was {errorCounter} while trying to save the course settings in the database.
Please check the following validation feedbacks and reflect them in your course settings:"
values={{ errorCounter: <strong>{errorList.length} validation error </strong> }}
/>
</p>
<hr />
<ul className="p-0">
{errorList.map((settingName) => (
<ModalErrorListItem
key={settingName.key}
settingName={settingName}
settingsData={settingsData}
values={{ errorCounter: <strong>{errorList.length} validation error </strong> }}
/>
))}
</ul>
</AlertModal>
);
</p>
<hr />
<ul className="p-0">
{errorList.map((settingName) => (
<ModalErrorListItem
key={settingName.key}
settingName={settingName}
settingsData={settingsData}
/>
))}
</ul>
</AlertModal>
);
};
ModalError.propTypes = {
intl: intlShape.isRequired,
isError: PropTypes.bool.isRequired,
handleUndoChanges: PropTypes.func.isRequired,
showErrorModal: PropTypes.func.isRequired,
@@ -60,4 +62,4 @@ ModalError.propTypes = {
settingsData: PropTypes.shape({}).isRequired,
};
export default injectIntl(ModalError);
export default ModalError;

View File

@@ -32,7 +32,7 @@
bottom: 0;
width: 100%;
padding: 0 .625rem;
z-index: $zindex-modal;
z-index: var(--pgn-elevation-modal-zindex);
}
.alert-proctoring-error {
@@ -66,13 +66,13 @@
.setting-sidebar-supplementary {
.setting-sidebar-supplementary-about {
.setting-sidebar-supplementary-about-title {
font: normal $font-weight-bold 1.125rem/1.5rem $font-family-base;
color: $headings-color;
font: normal var(--pgn-typography-font-weight-bold) 1.125rem/1.5rem var(--pgn-typography-font-family-base);
color: var(--pgn-color-headings-base);
margin-bottom: 1.25rem;
}
.setting-sidebar-supplementary-about-descriptions {
font: normal $font-weight-normal .875rem/1.5rem $font-family-base;
font: normal var(--pgn-typography-font-weight-normal) .875rem/1.5rem var(--pgn-typography-font-family-base);
color: $text-color-base;
}
}
@@ -81,16 +81,16 @@
list-style: none;
.setting-sidebar-supplementary-other-link {
font: normal $font-weight-normal .875rem/1.5rem $font-family-base;
font: normal var(--pgn-typography-font-weight-normal) .875rem/1.5rem var(--pgn-typography-font-family-base);
line-height: 1.5rem;
color: $info-500;
color: var(--pgn-color-info-500);
margin-bottom: .5rem;
}
}
.setting-sidebar-supplementary-other-title {
font: normal $font-weight-bold 1.125rem/1.5rem $font-family-base;
color: $headings-color;
font: normal var(--pgn-typography-font-weight-bold) 1.125rem/1.5rem var(--pgn-typography-font-family-base);
color: var(--pgn-color-headings-base);
margin-bottom: 1.25rem;
}
}
@@ -102,7 +102,7 @@
display: inline-block;
margin-right: 5px;
margin-bottom: 5px;
color: $danger;
color: var(--pgn-color-danger-base);
}
.modal-error-item-title {
@@ -113,12 +113,12 @@
.modal-popup-content {
max-width: 200px;
color: $white;
background-color: $black;
color: var(--pgn-color-white);
background-color: var(--pgn-color-black);
filter: drop-shadow(0 2px 4px rgba(0 0 0 / .15));
font-weight: 400;
}
.pgn__modal-popup__arrow::after {
border-top-color: $black;
border-top-color: var(--pgn-color-black);
}

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