Compare commits

...

528 Commits

Author SHA1 Message Date
renovate[bot]
31d690f03a fix(deps): update dependency react-datepicker to v9 2026-03-16 00:33:56 +00:00
edX requirements bot
1efd559786 chore: update browserslist DB (#2943)
Co-authored-by: abdullahwaheed <42172960+abdullahwaheed@users.noreply.github.com>
2026-03-16 00:30:22 +00:00
Navin Karkera
df79861685 fix: unit preview in course outline sidebar (#2940) 2026-03-13 13:42:39 -05:00
Rômulo Penido
24e1c73f6b feat: add UnitSidebarPagesContext (#2874) 2026-03-10 10:27:45 -05:00
renovate[bot]
449af65d01 chore(deps): update dependency oxlint-tsgolint to ^0.16.0 (#2930)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-09 09:40:45 -07:00
edX requirements bot
fce65c0215 chore: update browserslist DB (#2929)
Co-authored-by: abdullahwaheed <42172960+abdullahwaheed@users.noreply.github.com>
2026-03-09 00:26:52 +00:00
Muhammad Waleed
abcef4a502 fix: refresh search token after library (#2924)
creation for course creator access

  When a course creator creates a new library, the cached JWT token for
  Meilisearch needs to be refreshed to include the new library's access_id.
  Without this, newly added components won't appear in search results until
  the page is refreshed.

  This works in conjunction with the backend fix that creates SearchAccess
  records immediately on library creation.

  The fix invalidates the content_search query, triggering React Query to
  refetch the token with updated access permissions.
2026-03-05 14:17:27 +05:00
dependabot[bot]
5c1cdcf01c chore(deps): bump actions/upload-artifact from 6 to 7 (#2920)
Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 6 to 7.
- [Release notes](https://github.com/actions/upload-artifact/releases)
- [Commits](https://github.com/actions/upload-artifact/compare/v6...v7)

---
updated-dependencies:
- dependency-name: actions/upload-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>
2026-03-02 13:43:43 -08:00
dependabot[bot]
4dccc12883 chore(deps): bump actions/download-artifact from 7 to 8 (#2921) 2026-03-02 13:43:20 -08:00
renovate[bot]
f0e735b3a1 chore(deps): update codemirror (#2895)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-02 13:41:55 -08:00
Chris Chávez
091d9a1c3e refactor: Migrate courseExport from redux store to React Query (#2908) 2026-03-02 17:12:02 +00:00
edX requirements bot
7157d17a4e chore: update browserslist DB (#2918)
Co-authored-by: abdullahwaheed <42172960+abdullahwaheed@users.noreply.github.com>
2026-03-02 00:43:27 +00:00
Muhammad Anas
65096fde0e feat: add card in Pages and Resources to allow hiding the dates tab (#2834) 2026-02-27 15:37:41 -08:00
Braden MacDonald
af204c78a5 test: run only oxlint, not eslint (#2909) 2026-02-27 09:12:24 -08:00
Rômulo Penido
2e6209314f feat: add fullscreen button to XBlock editor (#2892) 2026-02-27 10:05:14 -05:00
Navin Karkera
060f7d4618 refactor(course-outline): replace thunks with react query, add typed APIs, and improve type usages (#2900)
* Replaces configure xblock and section highlights redux functions with react-query.
* Replaces section highlights thunks with react query
* Replaces duplicate block thunks
* Removes add subsection, unit redux functions
* Replaces scrollTo redux state to react-query based state.
* Replaces paste unit block redux functions
* Removes a lot of redux related functions as a result.
* Reduces API calls without compromising data integrity.
2026-02-27 09:23:46 -05:00
Chris Chávez
815b80a944 refactor: Migrate advancedSettings from the redux store to React Query (#2893) 2026-02-26 23:17:57 +00:00
Bryann Valderrama
e8cd7c2dcc feat: add team groups section in group configurations (#1728) 2026-02-26 14:02:31 -08:00
Chris Chávez
c4a09a2b43 refactor: Migrate courseImport from redux store to React query (#2902) 2026-02-26 14:34:13 -05:00
Areeb Sajjad
3599630cd7 fix: allow configure modal overflow to prevent datepicker clipping (#2901) 2026-02-25 17:19:18 -08:00
Chris Chávez
56726448fc Refactor: Move courseTeam from redux store to react query (#2888)
* refactor: Move `courseTeam` from redux store to react query

* fix: Broken types

* fix: Show user readable error

* fix: Broken coverage

* fix: Broken coverage

* refactor: Add default message error
2026-02-24 10:11:36 -05:00
renovate[bot]
b57386b9b6 chore(deps): update dependency oxlint-tsgolint to ^0.14.0 (#2896)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-23 22:46:26 +00:00
renovate[bot]
e9bf4566de chore(deps): update dependency @types/lodash to v4.17.24 (#2897)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-23 21:09:45 +00:00
edX requirements bot
1324b33789 chore: update browserslist DB (#2899)
Co-authored-by: abdullahwaheed <42172960+abdullahwaheed@users.noreply.github.com>
2026-02-23 21:09:26 +00:00
Taylor Payne
b256036527 fix: correct typos in user-facing messages (#2894) 2026-02-21 11:43:13 -08:00
Navin Karkera
ee6006e0e3 refactor(course-outline): improve query cache handling and remove redux thunks (#2884)
- Centralizes and reuses query cache handling logic:
Introduces a `ParentIds` type (src/generic/types.ts) and standardizes its use across data/API hooks for updating or invalidating parent/child query caches.
- Ensures cache coherence using `cancelQueries` before updating query data:
Before calling `setQueryData` for any block, any inflight queries are cancelled to prevent race conditions and stale UI.
- Simplifies post-sync/invalidation flows:
Removes Redux thunk usages in favor of direct query invalidations using React Query APIs within course outline cards, sidebars, publish modal, and `unlinkmodal`.
- Refactors data types for clarity:
Splits XBlock into `XBlockBase` and derived interfaces so the presence of `childInfo` is explicit.
- Cleans up redundant code and props:
Removes unnecessary `memoization`, `useDispatch` imports, and duplicate logic in React components.
2026-02-20 13:10:28 -05:00
MuPp3t33r
8f8c6d8dd2 refactor: update relative path to absolute path alias (#2891) 2026-02-20 09:10:25 -08:00
Chris Chávez
7c1eb59f18 feat: Make selectable component cards & Component Info Sidebar [FC-0114] (#2880)
- Changes in the Unit sidebar context to enable selected components
- Implements the component info sidebar.
- Implements the container/component selection when opening the align sidebar
2026-02-19 17:02:26 -05:00
Rodrigo Mendez
5ccf39d130 feat: Add validation for Advanced Settings permissions using openedx-authz (#2869)
* feat: Add validation for Advanced Settings permissions using openedx-authz

* squash!: Increase test coverage

* squash!: Fix lint issues

* squash!: Validate advanced settings permission on HelpSidebar

* squash!: Increase test coverage

* squash!: Attend PR comments
2026-02-19 14:39:04 -06:00
Chris Chávez
42f26e7404 refactor: Move redux to react query in course checklist (#2870) 2026-02-19 13:10:18 -05:00
renovate[bot]
01ddf0d2ad chore(deps): update dependency @edx/frontend-platform to v8.5.5 (#2882)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-18 23:20:06 +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
1811 changed files with 78776 additions and 49469 deletions

12
.env
View File

@@ -36,14 +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
# "Multi-level" blocks are unsupported in libraries
# TODO: Missing support for ORA2
LIBRARY_UNSUPPORTED_BLOCKS="conditional,step-builder,problem-builder,openassessment"
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,8 +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
# "Multi-level" blocks are unsupported in libraries
LIBRARY_UNSUPPORTED_BLOCKS="conditional,step-builder,problem-builder"
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,12 +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
# "Multi-level" blocks are unsupported in libraries
# TODO: Missing support for ORA2
LIBRARY_UNSUPPORTED_BLOCKS="conditional,step-builder,problem-builder,openassessment"
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

@@ -11,13 +11,13 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
- uses: actions/checkout@v6
- uses: actions/setup-node@v6
with:
node-version-file: '.nvmrc'
- run: make validate.ci
- name: Archive code coverage results
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v7
with:
name: code-coverage-report
path: coverage/*.*
@@ -25,13 +25,15 @@ jobs:
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@v8
with:
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 }}

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

@@ -51,7 +51,9 @@ validate-no-uncommitted-package-lock-changes:
validate:
make validate-no-uncommitted-package-lock-changes
npm run i18n_extract
npm run lint -- --max-warnings 0
# We are trying out oxlint. Now that it's been working well for a while with both oxlint and eslint, we have disabled
# eslint, and after a few weeks we'll evaluate whether any problems are slipping through if only oxlint is used.
npm run oxlint
npm run types
npm run test:ci
npm run build

View File

@@ -40,7 +40,7 @@ Cloning and Setup
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>`_.
@@ -97,7 +97,7 @@ Troubleshooting
* 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, 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>`__.
@@ -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.
@@ -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

@@ -14,6 +14,6 @@ metadata:
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',
],
});

9686
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",
@@ -34,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",
@@ -43,13 +44,14 @@
"@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.3.0",
"@edx/frontend-component-header": "^6.2.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.3.1",
"@edx/openedx-atlas": "^0.6.0",
"@edx/frontend-platform": "^8.4.0",
"@edx/openedx-atlas": "^0.7.0",
"@openedx-plugins/course-app-calculator": "file:plugins/course-apps/calculator",
"@openedx-plugins/course-app-dates": "file:plugins/course-apps/dates",
"@openedx-plugins/course-app-edxnotes": "file:plugins/course-apps/edxnotes",
"@openedx-plugins/course-app-learning_assistant": "file:plugins/course-apps/learning_assistant",
"@openedx-plugins/course-app-live": "file:plugins/course-apps/live",
@@ -59,65 +61,65 @@
"@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.3.3",
"@openedx/frontend-plugin-framework": "^1.6.0",
"@openedx/frontend-slot-footer": "^1.2.0",
"@openedx/paragon": "^22.16.0",
"@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": "^18.3.1",
"react-datepicker": "^4.13.0",
"react-datepicker": "^9.0.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.27.0",
"react-router-dom": "6.27.0",
"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": "^4.0.0",
"@edx/stylelint-config-edx": "2.3.3",
"@edx/typescript-config": "^1.0.1",
"@testing-library/jest-dom": "^6.6.3",
"@testing-library/react": "^16.2.0",
"@testing-library/user-event": "^13.2.1",
"@types/lodash": "^4.17.7",
"axios-mock-adapter": "1.22.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",
"jest-canvas-mock": "^2.5.2",
"jest-expect-message": "^1.1.3",
"oxlint": "^1.42.0",
"oxlint-tsgolint": "^0.16.0",
"react-test-renderer": "^18.3.1",
"redux-mock-store": "^1.5.4"
}

View File

@@ -0,0 +1,29 @@
import React from 'react';
import { useIntl } from '@edx/frontend-platform/i18n';
import AppSettingsModal from 'CourseAuthoring/pages-and-resources/app-settings-modal/AppSettingsModal';
import messages from './messages';
type DatesSettingsProps = {
onClose: () => void;
};
const DatesSettings: React.FC<DatesSettingsProps> = ({ onClose }) => {
const intl = useIntl();
return (
<AppSettingsModal
appId="dates"
title={intl.formatMessage(messages.heading)}
enableAppHelp={intl.formatMessage(messages.enableAppHelp)}
enableAppLabel={intl.formatMessage(messages.enableAppLabel)}
learnMoreText={intl.formatMessage(messages.learnMore)}
onClose={onClose}
validationSchema={{}}
initialValues={{}}
onSettingsSave={async () => true}
/>
);
};
export default DatesSettings;

View File

@@ -0,0 +1,26 @@
import { defineMessages } from '@edx/frontend-platform/i18n';
const messages = defineMessages({
heading: {
id: 'course-authoring.pages-resources.dates.heading',
defaultMessage: 'Configure dates',
description: 'Heading for the Dates settings modal shown in Pages & Resources.',
},
enableAppLabel: {
id: 'course-authoring.pages-resources.dates.enable-app.label',
defaultMessage: 'Dates',
description: 'Label for the toggle that enables the Dates experience.',
},
enableAppHelp: {
id: 'course-authoring.pages-resources.dates.enable-app.help',
defaultMessage: 'Show the Dates tab in course navigation, where learners can view important course dates.',
description: 'Helper text explaining what enabling the Dates experience does.',
},
learnMore: {
id: 'course-authoring.pages-resources.dates.learn-more',
defaultMessage: 'Learn more about dates',
description: 'Link text that leads to documentation about the Dates experience.',
},
});
export default messages;

View File

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

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

@@ -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');

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,24 +491,22 @@ 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 () => {
render(intlWrapper(<IntlProctoredExamSettings {...defaultProps} />));
render(renderComponent(<ProctoredExamSettings {...defaultProps} />));
const spinner = await screen.findByRole('status');
expect(spinner.textContent).toEqual('Loading...');
});
@@ -554,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.'),
@@ -566,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.'),
@@ -578,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'),
@@ -597,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);
@@ -606,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);
@@ -622,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');
@@ -637,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');
@@ -651,7 +632,6 @@ describe('ProctoredExamSettings', () => {
enable_proctored_exams: true,
allow_proctoring_opt_out: false,
proctoring_provider: 'mockproc',
create_zendesk_tickets: true,
},
});
@@ -665,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' } });
@@ -692,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',
},
});
@@ -706,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
@@ -722,7 +702,6 @@ describe('ProctoredExamSettings', () => {
enable_proctored_exams: true,
allow_proctoring_opt_out: false,
proctoring_provider: 'mockproc',
create_zendesk_tickets: true,
},
});
@@ -744,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
@@ -762,7 +741,6 @@ describe('ProctoredExamSettings', () => {
enable_proctored_exams: true,
allow_proctoring_opt_out: false,
proctoring_provider: 'mockproc',
create_zendesk_tickets: true,
},
});
@@ -780,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);
@@ -798,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);
@@ -816,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);
@@ -835,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);
@@ -862,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,9 +17,9 @@ import messages from './messages';
setupYupExtensions();
const TeamSettings = ({
intl,
onClose,
}) => {
const intl = useIntl();
const [teamsConfiguration, saveSettings] = useAppSetting('teamsConfiguration');
const blankNewGroup = {
name: '',
@@ -107,6 +107,7 @@ const TeamSettings = ({
)
.when('enabled', {
is: true,
// oxlint-disable-next-line unicorn/no-thenable
then: Yup.array().min(1),
})
.default([])
@@ -166,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,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

@@ -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,172 @@
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 { useSelector } from 'react-redux';
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 { 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;
handleAddAndOpenUnit: ReturnType<typeof useCreateCourseBlock>;
handleAddBlock: 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 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);
}
};
/**
* import a unit block from library and redirect user to this unit page.
*/
const handleAddAndOpenUnit = useCreateCourseBlock(courseId, openUnitPage);
const handleAddBlock = useCreateCourseBlock(courseId);
const context = useMemo<CourseAuthoringContextData>(() => ({
courseId,
courseUsageKey,
courseDetails,
courseDetailStatus,
canChangeProviders,
handleAddBlock,
handleAddAndOpenUnit,
getUnitUrl,
openUnitPage,
isUnlinkModalOpen,
openUnlinkModal,
closeUnlinkModal,
currentUnlinkModalData,
isPublishModalOpen,
currentPublishModalData,
openPublishModal,
closePublishModal,
currentSelection,
setCurrentSelection,
}), [
courseId,
courseUsageKey,
courseDetails,
courseDetailStatus,
canChangeProviders,
handleAddBlock,
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

@@ -4,9 +4,9 @@ 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, fetchWaffleFlags } 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/';
@@ -19,6 +19,12 @@ jest.mock('react-router-dom', () => ({
let axiosMock;
let store;
const renderComponent = children => render(
<CourseAuthoringProvider courseId={courseId}>
{children}
</CourseAuthoringProvider>,
);
beforeEach(async () => {
const mocks = initializeMocks();
store = mocks.reduxStore;
@@ -26,7 +32,6 @@ beforeEach(async () => {
axiosMock
.onGet(getApiWaffleFlagsUrl(courseId))
.reply(200, {});
await executeThunk(fetchWaffleFlags(courseId), store.dispatch);
});
describe('Editor Pages Load no header', () => {
@@ -36,14 +41,13 @@ 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(
<CourseAuthoringPage courseId={courseId}>
<PagesAndResources courseId={courseId} />
const wrapper = renderComponent(
<CourseAuthoringPage>
<PagesAndResources />
</CourseAuthoringPage>
,
);
@@ -52,9 +56,9 @@ describe('Editor Pages Load no header', () => {
test('renders loading wheel on non editor pages', async () => {
mockPathname = '/evilguy/';
await mockStoreSuccess();
const wrapper = render(
<CourseAuthoringPage courseId={courseId}>
<PagesAndResources courseId={courseId} />
const wrapper = renderComponent(
<CourseAuthoringPage>
<PagesAndResources />
</CourseAuthoringPage>
,
);
@@ -71,7 +75,6 @@ describe('Course authoring page', () => {
).reply(404, {
response: { status: 404 },
});
await executeThunk(fetchCourseDetail(courseId), store.dispatch);
};
const mockStoreError = async () => {
axiosMock.onGet(
@@ -79,11 +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(<CourseAuthoringPage courseId={courseId} />);
const wrapper = renderComponent(<CourseAuthoringPage />);
expect(await wrapper.findByTestId('notFoundAlert')).toBeInTheDocument();
});
test('does not render not found page on other kinds of error', async () => {
@@ -93,8 +95,8 @@ 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(
<CourseAuthoringPage courseId={courseId}>
const wrapper = renderComponent(
<CourseAuthoringPage>
<div data-testid={contentTestId} />
</CourseAuthoringPage>
,
@@ -102,4 +104,20 @@ describe('Course authoring page', () => {
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,41 +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 { StudioFooterSlot } from '@openedx/frontend-slot-footer';
import { StudioFooterSlot } from '@edx/frontend-component-footer';
import Header from './header';
import { fetchCourseDetail, fetchWaffleFlags } from './data/thunks';
import { useModel } from './generic/model-store';
import NotFoundAlert from './generic/NotFoundAlert';
import PermissionDeniedAlert from './generic/PermissionDeniedAlert';
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));
dispatch(fetchWaffleFlags(courseId));
}, [courseId]);
useEffect(() => {
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');
@@ -62,6 +57,9 @@ const CourseAuthoringPage = ({ courseId, children }) => {
org={courseOrg}
title={courseTitle}
contextId={courseId}
containerProps={{
size: 'fluid',
}}
/>
)
)}
@@ -71,13 +69,4 @@ const CourseAuthoringPage = ({ courseId, children }) => {
);
};
CourseAuthoringPage.propTypes = {
children: PropTypes.node,
courseId: PropTypes.string.isRequired,
};
CourseAuthoringPage.defaultProps = {
children: null,
};
export default CourseAuthoringPage;

View File

@@ -1,149 +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 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';
/**
* 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="libraries"
element={<PageWrap><CourseLibraries 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><IframeProvider><CourseUnit courseId={courseId} /></IframeProvider></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="optimizer"
element={<PageWrap><CourseOptimizerPage 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,7 +1,5 @@
import CourseAuthoringRoutes from './CourseAuthoringRoutes';
import { executeThunk } from './utils';
import { getApiWaffleFlagsUrl } from './data/api';
import { fetchWaffleFlags } from './data/thunks';
import {
screen, initializeMocks, render, waitFor,
} from './testUtils';
@@ -11,7 +9,6 @@ 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', () => ({
@@ -51,12 +48,14 @@ jest.mock('./custom-pages/CustomPages', () => (props) => {
describe('<CourseAuthoringRoutes>', () => {
beforeEach(async () => {
const { axiosMock, reduxStore } = initializeMocks();
store = reduxStore;
const user = {
userId: 1,
username: 'username',
};
const { axiosMock } = initializeMocks({ user });
axiosMock
.onGet(getApiWaffleFlagsUrl(courseId))
.reply(200, {});
await executeThunk(fetchWaffleFlags(courseId), store.dispatch);
});
it('renders the PagesAndResources component when the pages and resources route is active', async () => {
@@ -66,11 +65,7 @@ describe('<CourseAuthoringRoutes>', () => {
);
await waitFor(() => {
expect(screen.getByText(pagesAndResourcesMockText)).toBeVisible();
expect(mockComponentFn).toHaveBeenCalledWith(
expect.objectContaining({
courseId,
}),
);
expect(mockComponentFn).toHaveBeenCalled();
});
});
@@ -98,11 +93,7 @@ describe('<CourseAuthoringRoutes>', () => {
await waitFor(() => {
expect(screen.queryByText(videoSelectorContainerMockText)).toBeInTheDocument();
expect(screen.queryByText(pagesAndResourcesMockText)).not.toBeInTheDocument();
expect(mockComponentFn).toHaveBeenCalledWith(
expect.objectContaining({
courseId,
}),
);
expect(mockComponentFn).toHaveBeenCalled();
});
});
});

View File

@@ -0,0 +1,186 @@
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';
import { CourseImportProvider } from './import-page/CourseImportContext';
import { CourseExportProvider } from './export-page/CourseExportContext';
/**
* 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>
<CourseImportProvider>
<CourseImportPage />
</CourseImportProvider>
</PageWrap>
)}
/>
<Route
path="export"
element={(
<PageWrap>
<CourseExportProvider>
<CourseExportPage />
</CourseExportProvider>
</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

@@ -1,3 +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 { StudioFooterSlot } from '@openedx/frontend-slot-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>
<StudioFooterSlot />
</>
);
};
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,164 +0,0 @@
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 { executeThunk } from '../utils';
import { advancedSettingsMock } from './__mocks__';
import { getCourseAdvancedSettingsApiUrl } from './data/api';
import { updateCourseAppSetting } from './data/thunks';
import AdvancedSettings from './AdvancedSettings';
import messages from './messages';
let axiosMock;
let store;
const mockPathname = '/foo-bar';
const courseId = '123';
// Mock the TextareaAutosize component
jest.mock('react-textarea-autosize', () => jest.fn((props) => (
<textarea
{...props}
onFocus={() => {}}
onBlur={() => {}}
/>
)));
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>
);
describe('<AdvancedSettings />', () => {
beforeEach(() => {
initializeMockApp({
authenticatedUser: {
userId: 3,
username: 'abc123',
administrator: true,
roles: [],
},
});
store = initializeStore();
axiosMock = new MockAdapter(getAuthenticatedHttpClient());
axiosMock
.onGet(`${getCourseAdvancedSettingsApiUrl(courseId)}?fetch_all=0`)
.reply(200, advancedSettingsMock);
});
it('should render without errors', async () => {
const { getByText } = render(<RootWrapper />);
await waitFor(() => {
expect(getByText(messages.headingSubtitle.defaultMessage)).toBeInTheDocument();
const advancedSettingsElement = getByText(messages.headingTitle.defaultMessage, {
selector: 'h2.sub-header-title',
});
expect(advancedSettingsElement).toBeInTheDocument();
expect(getByText(messages.policy.defaultMessage)).toBeInTheDocument();
expect(getByText(/Do not modify these policies unless you are familiar with their purpose./i)).toBeInTheDocument();
});
});
it('should render setting element', async () => {
const { getByText, queryByText } = render(<RootWrapper />);
await waitFor(() => {
const advancedModuleListTitle = getByText(/Advanced Module List/i);
expect(advancedModuleListTitle).toBeInTheDocument();
expect(queryByText('Certificate web/html view enabled')).toBeNull();
});
});
it('should change to onСhange', async () => {
const { getByLabelText } = render(<RootWrapper />);
await waitFor(() => {
const textarea = getByLabelText(/Advanced Module List/i);
expect(textarea).toBeInTheDocument();
fireEvent.change(textarea, { target: { value: '[1, 2, 3]' } });
expect(textarea.value).toBe('[1, 2, 3]');
});
});
it('should display a warning alert', async () => {
const { getByLabelText, getByText } = render(<RootWrapper />);
await waitFor(() => {
const textarea = getByLabelText(/Advanced Module List/i);
fireEvent.change(textarea, { target: { value: '[3, 2, 1]' } });
expect(getByText(messages.buttonCancelText.defaultMessage)).toBeInTheDocument();
expect(getByText(messages.buttonSaveText.defaultMessage)).toBeInTheDocument();
expect(getByText(messages.alertWarning.defaultMessage)).toBeInTheDocument();
expect(getByText(messages.alertWarningDescriptions.defaultMessage)).toBeInTheDocument();
});
});
it('should display a tooltip on clicking on the icon', async () => {
const { getByLabelText, getByText } = render(<RootWrapper />);
await waitFor(() => {
const button = getByLabelText(/Show help text/i);
fireEvent.click(button);
expect(getByText(/Enter the names of the advanced modules to use in your course./i)).toBeInTheDocument();
});
});
it('should change deprecated button text ', async () => {
const { getByText } = render(<RootWrapper />);
await waitFor(() => {
const showDeprecatedItemsBtn = getByText(/Show Deprecated Settings/i);
expect(showDeprecatedItemsBtn).toBeInTheDocument();
fireEvent.click(showDeprecatedItemsBtn);
expect(getByText(/Hide Deprecated Settings/i)).toBeInTheDocument();
});
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 />);
let textarea;
await waitFor(() => {
textarea = getByLabelText(/Advanced Module List/i);
});
fireEvent.change(textarea, { target: { value: '[3, 2, 1]' } });
expect(textarea.value).toBe('[3, 2, 1]');
fireEvent.click(getByText(messages.buttonCancelText.defaultMessage));
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 />);
let textarea;
await waitFor(() => {
textarea = getByLabelText(/Advanced Module List/i);
});
fireEvent.change(textarea, { target: { value: '[3, 2, 1,' } });
expect(textarea.value).toBe('[3, 2, 1,');
fireEvent.click(getByText('Save changes'));
fireEvent.click(getByText('Change manually'));
expect(textarea.value).toBe('[3, 2, 1,');
});
it('should show success alert after save', async () => {
const { getByLabelText, getByText } = render(<RootWrapper />);
let textarea;
await waitFor(() => {
textarea = getByLabelText(/Advanced Module List/i);
});
fireEvent.change(textarea, { target: { value: '[3, 2, 1]' } });
expect(textarea.value).toBe('[3, 2, 1]');
axiosMock
.onPatch(`${getCourseAdvancedSettingsApiUrl(courseId)}`)
.reply(200, {
...advancedSettingsMock,
advancedModules: {
...advancedSettingsMock.advancedModules,
value: [3, 2, 1],
},
});
fireEvent.click(getByText('Save changes'));
await executeThunk(updateCourseAppSetting(courseId, [3, 2, 1]), store.dispatch);
expect(getByText('Your policy changes have been saved.')).toBeInTheDocument();
});
});

View File

@@ -0,0 +1,190 @@
import userEvent from '@testing-library/user-event';
import { CourseAuthoringProvider } from '@src/CourseAuthoringContext';
import { useUserPermissions } from '@src/authz/data/apiHooks';
import { mockWaffleFlags } from '@src/data/apiHooks.mock';
import {
render as baseRender,
fireEvent,
initializeMocks,
screen,
} from '@src/testUtils';
import { advancedSettingsMock } from './__mocks__';
import { getCourseAdvancedSettingsApiUrl } from './data/api';
import AdvancedSettings from './AdvancedSettings';
import messages from './messages';
let axiosMock;
const mockPathname = '/foo-bar';
const courseId = '123';
// Mock the TextareaAutosize component
jest.mock('react-textarea-autosize', () => jest.fn((props) => (
<textarea
{...props}
onFocus={() => { }}
/>
)));
jest.mock('@src/authz/data/apiHooks', () => ({
useUserPermissions: jest.fn(),
}));
const render = () => baseRender(
<CourseAuthoringProvider courseId={courseId}>
<AdvancedSettings />
</CourseAuthoringProvider>,
{ path: mockPathname },
);
describe('<AdvancedSettings />', () => {
beforeEach(() => {
const mocks = initializeMocks();
axiosMock = mocks.axiosMock;
axiosMock
.onGet(`${getCourseAdvancedSettingsApiUrl(courseId)}?fetch_all=0`)
.reply(200, advancedSettingsMock);
jest.mocked(useUserPermissions).mockReturnValue({
isLoading: false,
data: { canManageAdvancedSettings: true },
} as unknown as ReturnType<typeof useUserPermissions>);
});
it('should render placeholder when settings fetch returns 403', async () => {
axiosMock
.onGet(`${getCourseAdvancedSettingsApiUrl(courseId)}?fetch_all=0`)
.reply(403);
render();
expect(await screen.findByText(/Under Construction/i)).toBeInTheDocument();
});
it('should render without errors', async () => {
render();
expect(await screen.findByText(messages.headingSubtitle.defaultMessage)).toBeInTheDocument();
expect(screen.getByText(messages.headingTitle.defaultMessage, {
selector: 'h2.sub-header-title',
})).toBeInTheDocument();
expect(screen.getByText(messages.policy.defaultMessage)).toBeInTheDocument();
expect(screen.getByText(/Do not modify these policies unless you are familiar with their purpose./i)).toBeInTheDocument();
});
it('should render setting element', async () => {
render();
expect(await screen.findByText(/Advanced Module List/i)).toBeInTheDocument();
expect(screen.queryByText('Certificate web/html view enabled')).toBeNull();
});
it('should change to onСhange', async () => {
render();
const textarea = await screen.findByLabelText(/Advanced Module List/i);
expect(textarea).toBeInTheDocument();
fireEvent.change(textarea, { target: { value: '[1, 2, 3]' } });
expect(textarea).toHaveValue('[1, 2, 3]');
});
it('should display a warning alert', async () => {
render();
const textarea = await screen.findByLabelText(/Advanced Module List/i);
fireEvent.change(textarea, { target: { value: '[3, 2, 1]' } });
expect(screen.getByText(messages.buttonCancelText.defaultMessage)).toBeInTheDocument();
expect(screen.getByText(messages.buttonSaveText.defaultMessage)).toBeInTheDocument();
expect(screen.getByText(messages.alertWarning.defaultMessage)).toBeInTheDocument();
expect(screen.getByText(messages.alertWarningDescriptions.defaultMessage)).toBeInTheDocument();
});
it('should display a tooltip on clicking on the icon', async () => {
const user = userEvent.setup();
render();
const button = await screen.findByLabelText(/Show help text/i);
await user.click(button);
expect(screen.getByText(/Enter the names of the advanced modules to use in your course./i)).toBeInTheDocument();
});
it('should change deprecated button text', async () => {
const user = userEvent.setup();
render();
const showDeprecatedItemsBtn = await screen.findByText(/Show Deprecated Settings/i);
expect(showDeprecatedItemsBtn).toBeInTheDocument();
await user.click(showDeprecatedItemsBtn);
expect(screen.getByText(/Hide Deprecated Settings/i)).toBeInTheDocument();
expect(screen.getByText('Certificate web/html view enabled')).toBeInTheDocument();
});
it('should reset to default value on click on Cancel button', async () => {
const user = userEvent.setup();
render();
const textarea = await screen.findByLabelText(/Advanced Module List/i);
fireEvent.change(textarea, { target: { value: '[3, 2, 1]' } });
expect(textarea).toHaveValue('[3, 2, 1]');
await user.click(screen.getByText(messages.buttonCancelText.defaultMessage));
expect(textarea).toHaveValue('[]');
});
it('should update the textarea value and display the updated value after clicking "Change manually"', async () => {
const user = userEvent.setup();
render();
const textarea = await screen.findByLabelText(/Advanced Module List/i);
fireEvent.change(textarea, { target: { value: '[3, 2, 1,' } });
fireEvent.blur(textarea);
expect(textarea).toHaveValue('[3, 2, 1,');
await user.click(screen.getByText('Save changes'));
await user.click(await screen.findByText('Change manually'));
expect(textarea).toHaveValue('[3, 2, 1,');
});
it('should show success alert after save', async () => {
const user = userEvent.setup();
render();
const textarea = await screen.findByLabelText(/Advanced Module List/i);
fireEvent.change(textarea, { target: { value: '[3, 2, 1]' } });
expect(textarea).toHaveValue('[3, 2, 1]');
axiosMock
.onPatch(`${getCourseAdvancedSettingsApiUrl(courseId)}`)
.reply(200, {
...advancedSettingsMock,
advancedModules: {
...advancedSettingsMock.advancedModules,
value: [3, 2, 1],
},
});
await user.click(screen.getByText('Save changes'));
expect(screen.getByText('Your policy changes have been saved.')).toBeInTheDocument();
});
it('should show error modal on save failure', async () => {
const user = userEvent.setup();
render();
const textarea = await screen.findByLabelText(/Advanced Module List/i);
fireEvent.change(textarea, { target: { value: '[3, 2, 1]' } });
axiosMock
.onPatch(`${getCourseAdvancedSettingsApiUrl(courseId)}`)
.reply(500);
await user.click(screen.getByText('Save changes'));
expect(await screen.findByText('Validation error while saving')).toBeInTheDocument();
});
it('should render without errors when authz.enable_course_authoring flag is enabled and the user is authorized', async () => {
mockWaffleFlags({ enableAuthzCourseAuthoring: true });
jest.mocked(useUserPermissions).mockReturnValue({
isLoading: false,
data: { canManageAdvancedSettings: true },
} as unknown as ReturnType<typeof useUserPermissions>);
render();
expect(await screen.findByText(messages.headingSubtitle.defaultMessage)).toBeInTheDocument();
expect(screen.getByText(messages.headingTitle.defaultMessage, {
selector: 'h2.sub-header-title',
})).toBeInTheDocument();
expect(screen.getByText(messages.policy.defaultMessage)).toBeInTheDocument();
expect(screen.getByText(/Do not modify these policies unless you are familiar with their purpose./i)).toBeInTheDocument();
});
it('should show permission alert when authz.enable_course_authoring flag is enabled and the user is not authorized', async () => {
mockWaffleFlags({ enableAuthzCourseAuthoring: true });
jest.mocked(useUserPermissions).mockReturnValue({
isLoading: false,
data: { canManageAdvancedSettings: false },
} as unknown as ReturnType<typeof useUserPermissions>);
render();
expect(await screen.findByTestId('permissionDeniedAlert')).toBeInTheDocument();
});
});

View File

@@ -1,58 +1,73 @@
import React, { useEffect, useState } from 'react';
import PropTypes from 'prop-types';
import { useDispatch, useSelector } from 'react-redux';
import { useEffect, useState } from 'react';
import { Helmet } from 'react-helmet';
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 Placeholder from '../editors/Placeholder';
import { FormattedMessage, useIntl } from '@edx/frontend-platform/i18n';
import { useCourseAuthoringContext } from '@src/CourseAuthoringContext';
import { useWaffleFlags } from '@src/data/apiHooks';
import { useUserPermissions } from '@src/authz/data/apiHooks';
import { COURSE_PERMISSIONS } from '@src/authz/constants';
import PermissionDeniedAlert from 'CourseAuthoring/generic/PermissionDeniedAlert';
import AlertProctoringError from '@src/generic/AlertProctoringError';
import { LoadingSpinner } from '@src/generic/Loading';
import InternetConnectionAlert from '@src/generic/internet-connection-alert';
import { parseArrayOrObjectValues } from '@src/utils';
import { RequestStatus } from '@src/data/constants';
import SubHeader from '@src/generic/sub-header/SubHeader';
import AlertMessage from '@src/generic/alert-message';
import getPageHeadTitle from '@src/generic/utils';
import Placeholder from '@src/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';
import SubHeader from '../generic/sub-header/SubHeader';
import AlertMessage from '../generic/alert-message';
import { fetchCourseAppSettings, updateCourseAppSetting, fetchProctoringExamErrors } from './data/thunks';
import {
getCourseAppSettings, getSavingStatus, getProctoringExamErrors, getSendRequestErrors, getLoadingStatus,
} from './data/selectors';
import SettingCard from './setting-card/SettingCard';
import SettingsSidebar from './settings-sidebar/SettingsSidebar';
import validateAdvancedSettingsData from './utils';
import messages from './messages';
import ModalError from './modal-error/ModalError';
import getPageHeadTitle from '../generic/utils';
import { useCourseAdvancedSettings, useProctoringExamErrors, useUpdateCourseAdvancedSettings } from './data/apiHooks';
const AdvancedSettings = ({ intl, courseId }) => {
const dispatch = useDispatch();
const AdvancedSettings = () => {
const intl = useIntl();
const [saveSettingsPrompt, showSaveSettingsPrompt] = useState(false);
const [showDeprecated, setShowDeprecated] = useState(false);
const [errorModal, showErrorModal] = useState(false);
const [editedSettings, setEditedSettings] = useState({});
const [errorFields, setErrorFields] = useState([]);
const [showSuccessAlert, setShowSuccessAlert] = useState(false);
const [isQueryPending, setIsQueryPending] = useState(false);
const [isEditableState, setIsEditableState] = useState(false);
const [hasInternetConnectionError, setInternetConnectionError] = useState(false);
const courseDetails = useModel('courseDetails', courseId);
document.title = getPageHeadTitle(courseDetails?.name, intl.formatMessage(messages.headingTitle));
const { courseId, courseDetails } = useCourseAuthoringContext();
useEffect(() => {
dispatch(fetchCourseAppSettings(courseId));
dispatch(fetchProctoringExamErrors(courseId));
}, [courseId]);
const waffleFlags = useWaffleFlags(courseId);
const isAuthzEnabled = waffleFlags.enableAuthzCourseAuthoring;
const { isLoading: isLoadingUserPermissions, data: userPermissions } = useUserPermissions({
canManageAdvancedSettings: {
action: COURSE_PERMISSIONS.MANAGE_ADVANCED_SETTINGS,
scope: courseId,
},
}, isAuthzEnabled);
const advancedSettingsData = useSelector(getCourseAppSettings);
const savingStatus = useSelector(getSavingStatus);
const proctoringExamErrors = useSelector(getProctoringExamErrors);
const settingsWithSendErrors = useSelector(getSendRequestErrors) || {};
const loadingSettingsStatus = useSelector(getLoadingStatus);
const {
data: advancedSettingsData = {},
isPending: isPendingSettingsStatus,
failureReason: settingsStatusError,
} = useCourseAdvancedSettings(courseId);
const isLoading = loadingSettingsStatus === RequestStatus.IN_PROGRESS;
const {
data: proctoringExamErrors = {},
} = useProctoringExamErrors(courseId);
const updateMutation = useUpdateCourseAdvancedSettings(courseId);
const {
isPending: isQueryPending,
isSuccess: isQuerySuccess,
error: queryError,
} = updateMutation;
const isLoading = isPendingSettingsStatus || (isAuthzEnabled && isLoadingUserPermissions);
const updateSettingsButtonState = {
labels: {
default: intl.formatMessage(messages.buttonSaveText),
@@ -60,30 +75,34 @@ const AdvancedSettings = ({ intl, courseId }) => {
},
disabledStates: ['pending'],
};
const {
proctoringErrors,
mfeProctoredExamSettingsUrl,
} = proctoringExamErrors;
useEffect(() => {
if (savingStatus === RequestStatus.SUCCESSFUL) {
setIsQueryPending(false);
if (isQuerySuccess) {
setShowSuccessAlert(true);
setIsEditableState(false);
setTimeout(() => setShowSuccessAlert(false), 15000);
window.scrollTo({ top: 0, behavior: 'smooth' });
showSaveSettingsPrompt(false);
} else if (savingStatus === RequestStatus.FAILED && !hasInternetConnectionError) {
setErrorFields(settingsWithSendErrors);
} else if (queryError && !hasInternetConnectionError) {
// @ts-ignore
setErrorFields(queryError?.response?.data ?? []);
showErrorModal(true);
}
}, [savingStatus]);
}, [isQuerySuccess, queryError]);
if (isLoading) {
// eslint-disable-next-line react/jsx-no-useless-fragment
return <></>;
return (
<div className="row justify-content-center m-6">
<LoadingSpinner />
</div>
);
}
if (loadingSettingsStatus === RequestStatus.DENIED) {
if (settingsStatusError?.response?.status === 403) {
return (
<div className="row justify-content-center m-6">
<Placeholder />
@@ -105,31 +124,42 @@ const AdvancedSettings = ({ intl, courseId }) => {
const handleUpdateAdvancedSettingsData = () => {
const isValid = validateAdvancedSettingsData(editedSettings, setErrorFields, setEditedSettings);
if (isValid) {
setIsQueryPending(true);
setShowSuccessAlert(false);
updateMutation.mutate(parseArrayOrObjectValues(editedSettings));
} else {
showSaveSettingsPrompt(false);
showErrorModal(!errorModal);
}
};
/* istanbul ignore next */
const handleInternetConnectionFailed = () => {
setInternetConnectionError(true);
showSaveSettingsPrompt(false);
setShowSuccessAlert(false);
};
const handleQueryProcessing = () => {
setShowSuccessAlert(false);
dispatch(updateCourseAppSetting(courseId, parseArrayOrObjectValues(editedSettings)));
};
const handleManuallyChangeClick = (setToState) => {
showErrorModal(setToState);
showSaveSettingsPrompt(true);
};
// Show permission denied alert when authz is enabled and user doesn't have permission
const authzIsEnabledAndNoPermission = isAuthzEnabled
&& !isLoadingUserPermissions
&& !userPermissions?.canManageAdvancedSettings;
if (authzIsEnabledAndNoPermission) {
return <PermissionDeniedAlert />;
}
return (
<>
<Helmet>
<title>
{getPageHeadTitle(courseDetails?.name ?? '', intl.formatMessage(messages.headingTitle))}
</title>
</Helmet>
<Container size="xl" className="advanced-settings px-4">
<div className="setting-header mt-5">
{(proctoringErrors?.length > 0) && (
@@ -139,7 +169,11 @@ const AdvancedSettings = ({ intl, courseId }) => {
aria-hidden="true"
aria-labelledby={intl.formatMessage(messages.alertProctoringAriaLabelledby)}
aria-describedby={intl.formatMessage(messages.alertProctoringDescribedby)}
/>
>
{/* Empty children to satisfy the type checker */}
{/* eslint-disable-next-line react/jsx-no-useless-fragment */}
<></>
</AlertProctoringError>
)}
<TransitionReplace>
{showSuccessAlert ? (
@@ -192,8 +226,8 @@ const AdvancedSettings = ({ intl, courseId }) => {
defaultMessage="{visibility} deprecated settings"
values={{
visibility:
showDeprecated ? intl.formatMessage(messages.deprecatedButtonHideText)
: intl.formatMessage(messages.deprecatedButtonShowText),
showDeprecated ? intl.formatMessage(messages.deprecatedButtonHideText)
: intl.formatMessage(messages.deprecatedButtonShowText),
}}
/>
</Button>
@@ -235,9 +269,8 @@ const AdvancedSettings = ({ intl, courseId }) => {
<div className="alert-toast">
{isQueryPending && (
<InternetConnectionAlert
isFailed={savingStatus === RequestStatus.FAILED}
isFailed={Boolean(queryError)}
isQueryPending={isQueryPending}
onQueryProcessing={handleQueryProcessing}
onInternetConnectionFailed={handleInternetConnectionFailed}
/>
)}
@@ -248,18 +281,18 @@ const AdvancedSettings = ({ intl, courseId }) => {
aria-describedby={intl.formatMessage(messages.alertWarningAriaDescribedby)}
role="dialog"
actions={[
!isQueryPending && (
!isQueryPending ? (
<Button variant="tertiary" onClick={handleResetSettingsValues}>
{intl.formatMessage(messages.buttonCancelText)}
</Button>
),
) : /* istanbul ignore next */ null,
<StatefulButton
key="statefulBtn"
onClick={handleUpdateAdvancedSettingsData}
state={isQueryPending ? RequestStatus.PENDING : 'default'}
{...updateSettingsButtonState}
/>,
].filter(Boolean)}
].filter((action): action is JSX.Element => action !== null)}
variant="warning"
icon={Warning}
title={intl.formatMessage(messages.alertWarning)}
@@ -277,9 +310,4 @@ const AdvancedSettings = ({ intl, courseId }) => {
);
};
AdvancedSettings.propTypes = {
intl: intlShape.isRequired,
courseId: PropTypes.string.isRequired,
};
export default injectIntl(AdvancedSettings);
export default AdvancedSettings;

View File

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

View File

@@ -1,40 +0,0 @@
import { camelCaseObject, getConfig } from '@edx/frontend-platform';
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
import { convertObjectToSnakeCase } from '../../utils';
const getApiBaseUrl = () => getConfig().STUDIO_BASE_URL;
export const getCourseAdvancedSettingsApiUrl = (courseId) => `${getApiBaseUrl()}/api/contentstore/v0/advanced_settings/${courseId}`;
const getProctoringErrorsApiUrl = () => `${getApiBaseUrl()}/api/contentstore/v1/proctoring_errors/`;
/**
* Get's advanced setting for a course.
* @param {string} courseId
* @returns {Promise<Object>}
*/
export async function getCourseAdvancedSettings(courseId) {
const { data } = await getAuthenticatedHttpClient()
.get(`${getCourseAdvancedSettingsApiUrl(courseId)}?fetch_all=0`);
return camelCaseObject(data);
}
/**
* Updates advanced setting for a course.
* @param {string} courseId
* @param {object} settings
* @returns {Promise<Object>}
*/
export async function updateCourseAdvancedSettings(courseId, settings) {
const { data } = await getAuthenticatedHttpClient()
.patch(`${getCourseAdvancedSettingsApiUrl(courseId)}`, convertObjectToSnakeCase(settings));
return camelCaseObject(data);
}
/**
* Gets proctoring exam errors.
* @param {string} courseId
* @returns {Promise<Object>}
*/
export async function getProctoringExamErrors(courseId) {
const { data } = await getAuthenticatedHttpClient().get(`${getProctoringErrorsApiUrl()}${courseId}`);
return camelCaseObject(data);
}

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

@@ -0,0 +1,77 @@
import {
camelCaseObject,
getConfig,
} from '@edx/frontend-platform';
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
import { camelCase } from 'lodash';
import { convertObjectToSnakeCase } from '@src/utils';
const getApiBaseUrl = () => getConfig().STUDIO_BASE_URL;
export const getCourseAdvancedSettingsApiUrl = (courseId) => `${getApiBaseUrl()}/api/contentstore/v0/advanced_settings/${courseId}`;
const getProctoringErrorsApiUrl = () => `${getApiBaseUrl()}/api/contentstore/v1/proctoring_errors/`;
/**
* Get's advanced setting for a course.
*/
export async function getCourseAdvancedSettings(courseId: string): Promise<Record<string, any>> {
const { data } = await getAuthenticatedHttpClient()
.get(`${getCourseAdvancedSettingsApiUrl(courseId)}?fetch_all=0`);
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;
}
/**
* Updates advanced setting for a course.
*/
export async function updateCourseAdvancedSettings(
courseId: string,
settings: Record<string, any>,
): Promise<Record<string, any>> {
const { data } = await getAuthenticatedHttpClient()
.patch(`${getCourseAdvancedSettingsApiUrl(courseId)}`, convertObjectToSnakeCase(settings));
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;
}
/**
* Gets proctoring exam errors.
*/
export async function getProctoringExamErrors(courseId: string): Promise<Record<string, any>> {
const { data } = await getAuthenticatedHttpClient().get(`${getProctoringErrorsApiUrl()}${courseId}`);
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,56 @@
/* eslint-disable import/no-extraneous-dependencies */
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import { AxiosError } from 'axios';
import {
getCourseAdvancedSettings,
getProctoringExamErrors,
updateCourseAdvancedSettings,
} from './api';
export const advancedSettingsQueryKeys = {
all: ['advancedSettings'],
/** Base key for advanced settings specific to a courseId */
courseAdvancedSettings: (courseId: string) => [...advancedSettingsQueryKeys.all, courseId],
/** Key for proctoring exam errors specific to a courseId */
proctoringExamErrors: (courseId: string) => [...advancedSettingsQueryKeys.all, courseId, 'proctoringErrors'],
};
const sortSettingsByDisplayName = (settings: Record<string, any>): Record<string, any> => (
Object.fromEntries(Object.entries(settings).sort(
([, v1], [, v2]) => v1.displayName.localeCompare(v2.displayName),
))
);
/**
* Fetches the advanced settings for a course, sorted alphabetically by display name.
*/
export const useCourseAdvancedSettings = (courseId: string) => (
useQuery<Record<string, any>, AxiosError>({
queryKey: advancedSettingsQueryKeys.courseAdvancedSettings(courseId),
queryFn: () => getCourseAdvancedSettings(courseId),
select: sortSettingsByDisplayName,
})
);
/**
* Fetches the proctoring exam errors for a course.
*/
export const useProctoringExamErrors = (courseId: string) => (
useQuery({
queryKey: advancedSettingsQueryKeys.proctoringExamErrors(courseId),
queryFn: () => getProctoringExamErrors(courseId),
})
);
/**
* Returns a mutation to update the advanced settings for a course.
*/
export const useUpdateCourseAdvancedSettings = (courseId: string) => {
const queryClient = useQueryClient();
return useMutation<Record<string, any>, AxiosError, Record<string, any>>({
mutationFn: (settings: Record<string, any>) => updateCourseAdvancedSettings(courseId, settings),
onSettled: () => {
queryClient.invalidateQueries({ queryKey: advancedSettingsQueryKeys.courseAdvancedSettings(courseId) });
},
});
};

View File

@@ -1,5 +0,0 @@
export const getLoadingStatus = (state) => state.advancedSettings.loadingStatus;
export const getCourseAppSettings = state => state.advancedSettings.courseAppSettings;
export const getSavingStatus = (state) => state.advancedSettings.savingStatus;
export const getProctoringExamErrors = (state) => state.advancedSettings.proctoringErrors;
export const getSendRequestErrors = (state) => state.advancedSettings.sendRequestErrors.developer_message;

View File

@@ -1,48 +0,0 @@
/* eslint-disable no-param-reassign */
import { createSlice } from '@reduxjs/toolkit';
import { RequestStatus } from '../../data/constants';
const slice = createSlice({
name: 'advancedSettings',
initialState: {
loadingStatus: RequestStatus.IN_PROGRESS,
savingStatus: '',
courseAppSettings: {},
proctoringErrors: {},
sendRequestErrors: {},
},
reducers: {
updateLoadingStatus: (state, { payload }) => {
state.loadingStatus = payload.status;
},
updateSavingStatus: (state, { payload }) => {
state.savingStatus = payload.status;
},
fetchCourseAppsSettingsSuccess: (state, { payload }) => {
Object.assign(state.courseAppSettings, payload);
},
updateCourseAppsSettingsSuccess: (state, { payload }) => {
Object.assign(state.courseAppSettings, payload);
},
getDataSendErrors: (state, { payload }) => {
Object.assign(state.sendRequestErrors, payload);
},
fetchProctoringExamErrorsSuccess: (state, { payload }) => {
Object.assign(state.proctoringErrors, payload);
},
},
});
export const {
updateLoadingStatus,
updateSavingStatus,
getDataSendErrors,
fetchCourseAppsSettingsSuccess,
updateCourseAppsSettingsSuccess,
fetchProctoringExamErrorsSuccess,
} = slice.actions;
export const {
reducer,
} = slice;

View File

@@ -1,85 +0,0 @@
import { RequestStatus } from '../../data/constants';
import {
getCourseAdvancedSettings,
updateCourseAdvancedSettings,
getProctoringExamErrors,
} from './api';
import {
fetchCourseAppsSettingsSuccess,
updateCourseAppsSettingsSuccess,
updateLoadingStatus,
updateSavingStatus,
fetchProctoringExamErrorsSuccess,
getDataSendErrors,
} from './slice';
export function fetchCourseAppSettings(courseId) {
return async (dispatch) => {
dispatch(updateLoadingStatus({ status: RequestStatus.IN_PROGRESS }));
try {
const settingValues = await getCourseAdvancedSettings(courseId);
const sortedDisplayName = [];
Object.values(settingValues).forEach(value => {
const { displayName } = value;
sortedDisplayName.push(displayName);
});
const sortedSettingValues = {};
sortedDisplayName.sort().forEach((displayName => {
Object.entries(settingValues).forEach(([key, value]) => {
if (value.displayName === displayName) {
sortedSettingValues[key] = value;
}
});
}));
dispatch(fetchCourseAppsSettingsSuccess(sortedSettingValues));
dispatch(updateLoadingStatus({ status: RequestStatus.SUCCESSFUL }));
} catch (error) {
if (error.response && error.response.status === 403) {
dispatch(updateLoadingStatus({ courseId, status: RequestStatus.DENIED }));
} else {
dispatch(updateLoadingStatus({ courseId, status: RequestStatus.FAILED }));
}
}
};
}
export function updateCourseAppSetting(courseId, settings) {
return async (dispatch) => {
dispatch(updateSavingStatus({ status: RequestStatus.IN_PROGRESS }));
try {
const settingValues = await updateCourseAdvancedSettings(courseId, settings);
dispatch(updateCourseAppsSettingsSuccess(settingValues));
dispatch(updateSavingStatus({ status: RequestStatus.SUCCESSFUL }));
return true;
} catch (error) {
let errorData;
try {
const { customAttributes: { httpErrorResponseData } } = error;
errorData = JSON.parse(httpErrorResponseData);
} catch (err) {
errorData = {};
}
dispatch(getDataSendErrors(errorData));
dispatch(updateSavingStatus({ status: RequestStatus.FAILED }));
return false;
}
};
}
export function fetchProctoringExamErrors(courseId) {
return async (dispatch) => {
dispatch(updateSavingStatus({ status: RequestStatus.IN_PROGRESS }));
try {
const settingValues = await getProctoringExamErrors(courseId);
dispatch(fetchProctoringExamErrorsSuccess(settingValues));
return true;
} catch (error) {
dispatch(updateSavingStatus({ status: RequestStatus.FAILED }));
return false;
}
};
}

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;

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