Compare commits

...

1002 Commits

Author SHA1 Message Date
Asad Ali
14e122a672 fix: do not reload multiple tabs on block save (backport #2600) (#2704) 2025-12-01 18:26:06 -05:00
Asad Ali
f459f53343 fix: load sequences in unit page (#1867) (#2424)
This handles loading errors when opening the course unit page via direct link as an unauthorized user.

Co-authored-by: Ihor Romaniuk <ihor.romaniuk@raccoongang.com>
2025-09-08 22:23:06 -07:00
Asad Ali
a5a7d03d12 fix: allow thumbnail upload on Videos page if no thumbnail (#2388) (#2434)
* 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 20:45:27 -07:00
Chris Chávez
41fc478efe [Teak] style: Fixing nits about sync units [FC-0097] (#2320)
* Add a warning banner about units in the libraries sync page.
* Update the message in the sync unit modal.
* Stay visible the sync icon in the course outline.
* Add a tooltip to the edit (in normal and disabled mode) and sync button.
2025-08-28 11:15:24 -05:00
Muhammad Faraz Maqsood
06497bf85c 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-08-20 15:08:24 +05:00
Jacobo Dominguez
7e0b7f94e8 docs: (backport) adding comprehensive readme documentation for plugin slots (#2340) 2025-07-29 15:27:24 -07:00
Jansen Kantor
4bc34c268b fix: pages and resources plugins not rendered (#1885) 2025-07-22 13:26:38 +05:30
Muhammad Anas
2973614e3b fix: loading unit page directly from link after logging in in Teak (#2246)
This is a simple version of the fix for Teak; on master it was fixed with https://github.com/openedx/frontend-app-authoring/pull/1867
2025-07-09 09:35:58 -07:00
Brayan Cerón
bdc99fddc3 fix: clear selection on files & uploads page after deleting (backport) (#2228)
* 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-07-07 16:47:45 -07:00
José Ignacio Palma
92c59cbf0c fix: advanced-settings api should not camel-case return value (backport) (#2087)
* fix: advanced-settings api should not camel-case return value (#1581)

* 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 <fmaqsood@2u.com>
2025-06-19 09:06:31 -07:00
Arunmozhi
b6bd94c114 feat: add v2 CourseAuthoringUnitSidebarSlot (#2000) 2025-06-18 12:17:13 +05:30
Chris Chávez
c9896a8fe5 [Teak] fix: published name in unit sidebar in container picker & Issues on Inplace Editor (#2140)
Backport of fix: show unit published name in sidebar on content picker [FC-0090] #2100 
Backport of fix: Issue on the Inplace editor [FC-0090] #2101
2025-06-17 19:58:57 -05:00
bydawen
4ba8cde587 fix: (backport) text truncate issue in the search modal (#2151) 2025-06-16 14:43:02 -07:00
Diana Villalvazo
86d0a7e7db fix: remove icon and empty breadcrumb from libraries (#2129) (#2133) 2025-06-12 14:43:18 -07:00
Braden MacDonald
1968d146cd fix: (backport) enable markdown editor in libraries (#2098)
* fix: enable markdown editor for problems in libraries too

This fix is also achieved on master via 5991fd3997 / https://github.com/openedx/frontend-app-authoring/pull/2068 but this is a simpler fix, not a direct backport of that refactor.

* fix: remove duplicate markdown_edited save request (#2127)

Removes the unnecessary duplicate save  request of markdown_edited
value to the backend.

Part of: https://github.com/openedx/frontend-app-authoring/issues/2099
Backports: 62589aea50

---------

Co-authored-by: Muhammad Anas <88967643+Anas12091101@users.noreply.github.com>
2025-06-12 09:16:53 -07:00
Ihor Romaniuk
3e737b5b0d fix: (backport) remove an extra editing xblock modal on unit page (#2111) (#2130) 2025-06-11 13:25:47 -07:00
diana-villalvazo-wgu
fcdf1fdecb fix: files & uploads menu was truncated due to overflow-x (#2071) (#2077) 2025-06-05 19:41:21 +05:30
Victor Navarro
efb1a28b4d fix: Expand all now expands subsections (#2085) 2025-06-05 09:35:41 -03:00
Muhammad Anas
1ff5e5bdae fix: markdown editor issues in modal (#2076)
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

Backports https://github.com/openedx/frontend-app-authoring/pull/2074
2025-06-04 12:59:24 -04:00
Tony Busa
19ef80553a fix: backport changes for html button in text component markdown editor (#2065) 2025-06-04 17:51:05 +05:30
Rômulo Penido
2beb91c63b fix: set unit preview readonly on sidebar (#2008) (#2059)
Make the unit preview on the sidebar read-only and add `Truncate` to the `InplaceTextEditor`
2025-06-02 12:11:58 -05:00
Rômulo Penido
d325a92204 fix: selection card wiggle (#2047) 2025-05-29 14:06:35 -05:00
Jillian
7dfd93d4f1 fix: upstreamInfo is not always provided (#2041) (#2042)
(cherry picked from commit 3fc0f27d67)
2025-05-29 13:15:01 -05:00
Jillian
e34df7f270 fix: set maxHeight on TextEditor TinyMce widget [FC-0090] (#2024) (#2030)
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.

(cherry picked from commit c5f7d0cf3b)
2025-05-26 13:05:48 -05:00
Jillian
317bc757cf fix: refresh xblock inline after accepting/rejecting library sync (#2022) (#2028)
Instead of reloading the entire Unit after syncing changes from the
library, just reload the xblock that was changed.

(cherry picked from commit ac5574d2c4)
2025-05-23 14:03:57 -05:00
Chris Chávez
212a54f76e [Teak] fix: Inconsistent publish status filter menu placement & fix: Remove never published filter from component picker (#2021)
* fix: Inconsistent publish status filter menu placement (#1966)

* 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 10:22:53 -05:00
Daniel Valenzuela
944d1316ad fix: do open editor of new xblock when duplicating (#2017)
* feat: display editors as modals  (#1838)

* 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-22 10:04:35 -05:00
Rômulo Penido
dd731a0d19 fix: rename library publish button (#2015) 2025-05-21 18:18:26 -05:00
Rômulo Penido
976dfcaab7 fix: change InplaceTextEditor style and add optimistic update (#1953) (#2014)
* 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-21 17:33:23 -05:00
Navin Karkera
403dfa1e6b [Teak] backport #1949, #1999 and #2002 (#2006)
* 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

(cherry picked from commit 08ac1c0c4d)

* fix: search text flickering (#1999)

Fix flickering issue in search field.

(cherry picked from commit 6f3b7ab962)

* feat: open collection or unit page on double click only (#2002)

Opens collection or unit page only on double click.

(cherry picked from commit 503642be8c)
2025-05-21 17:20:16 -05:00
Navin Karkera
1919eb4845 fix: search modal refresh on typing (#1938) (#1948) 2025-05-14 13:15:24 -05:00
Chris Chávez
3d6e221f99 fix: Issue with read-only units in libraries & published version of units in library units picker (#1940)
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-13 12:53:32 -05:00
Rômulo Penido
fab786a6c6 fix: review/sync bugs [FC-0083] (#1905) (#1941)
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 14:42:34 -05:00
Rômulo Penido
a162929fd7 fix: improve focus/selected style on library authoring (#1918) (#1930)
Improves the focus and selected styles from the LibraryPage and UnitPage.
2025-05-12 12:05:28 -05:00
Jillian
6c4634ebbe fix: invalidate search results when publishing all changes in library (#1925) (#1927)
(cherry picked from commit cdb8016657)

Co-authored-by: Braden MacDonald <braden@opencraft.com>
2025-05-09 11:03:58 -05:00
Navin Karkera
79f865b328 fix: UX issues in unit page (#1913) (#1923)
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

(cherry picked from commit 8c3fab3792)
2025-05-08 10:15:44 -05:00
Rômulo Penido
d5e36cf2b8 fix: unit pages ux bugs [FC-0083] (#1884) (#1916)
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

It's a backport of https://github.com/openedx/frontend-app-authoring/pull/1884
2025-05-07 17:39:55 -05:00
Ihor Romaniuk
8ffafc094f fix: manage access modal on duplicated xblock (#1874) 2025-05-07 15:40:34 -03:00
Jillian
b375806fd2 perf: use Library search results to populate container card preview [FC-0083] [TEAK] (#1889)
* 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

(cherry picked from commit 0fdc460c5b)

* 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

(cherry picked from commit 24e469542d)

---------

Co-authored-by: Rômulo Penido <romulo.penido@gmail.com>
2025-05-02 10:18:20 -07:00
Navin Karkera
ab0e0d71c1 refactor: remove custom order function from course libraries list (#1865) (#1888)
(cherry picked from commit bc18fffedf)
2025-05-01 15:28:09 -07:00
Jillian
b30a1c8c5e fix: NaN library components are out of sync [FC-0083] (#1864) 2025-04-25 10:08:33 -05:00
Chris Chávez
855b44f745 feat: Add 'This unit can only be edited from the library' banner (#1860) 2025-04-24 20:50:29 -07:00
Jillian
2d55ba4ccc fix: allow units sourced from libraries to update their settings (#1863) 2025-04-24 17:58:37 -07:00
Chris Chávez
d62c4cf4f8 feat: Sync units in course outline [FC-0083] (#1850)
* Adds the sync button in unit cards in the course outline.
* Opens the compare previews.
* Functionality to sync units.
* Functionality to decline sync units.
2025-04-24 20:18:04 +00:00
Rômulo Penido
9824502278 feat: disallow edits to units in courses that are sourced from a library (#1833) 2025-04-24 19:20:46 +00:00
Chris Chávez
d6b51ecf0c feat: Add unit from library into course (#1829)
* feat: Initial worflow to add unit to course
* test: Add initial tests
* feat: Show only published units
* test: Update Subsection card test and ComponentPicker tests
* feat: Connect add unit from library API
* test: Test for Add unit from library in CourseOutline
* fix: create a new Vertical from a Library Unit
* docs: add a little note about avoiding 'vertical' where possible
* refactor: Use visibleTabs instead of showOnlyHomeTab

---------

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

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

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

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

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

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

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

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

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

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

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

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

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

---------

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

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

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

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

* chore: ignore coverage of modal fixer

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

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

**Accessibility**

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

**Performance**

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

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

The following events have been processed:

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

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

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

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

No changes were made to the UI.

Use cases covered:

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

* LibraryContext
* ComponentPickerContext
* SidebarContext
2024-12-12 13:14:45 -05:00
Brayan Cerón
c7e2bf9934 fix: find proper courses when searching (#1497)
When active/archived filters were on or there was selected any order filter, the search skipped these values and it was just returned the courses list without the respective filters. Additionally, when a search keyword was applied and a filter was selected, the keyword stayed stuck and the search list returned were not the appropriate
2024-12-09 14:44:02 -08:00
Daniel Valenzuela
73490a5741 fix: avoid changing url when removing filters (#1530)
* Makes the Active Tab Key independent from the URL, except for the initial load, where the active tab is set from the url.
*Avoids unnecessarily changing SearchParams: Due to a limitation of the useSearchParams react hook, which uses a memoized value for the URL that becomes stale after selecting a tab, it unexpectedly changes the URL value. Unfortunately there's no way to completely avoid this, so if there's a usageKey url param, the hook setter function will be called and the URL will revert to the stale memoized url.
2024-12-06 16:24:04 -05:00
Chris Chávez
d2d753203f fix: Update error messages when adding user to library [FC-0062] (#1543)
Updates the message error when the user doesn't exist when adding a new team member to a library.
2024-12-06 15:46:52 -05:00
Rômulo Penido
0e9025a670 fix: legacy library links were not working (#1548) 2024-12-06 18:50:53 +00:00
Rômulo Penido
2f1263ab5a fix: show/hide "new library" button based on separate v1/v2 permissions (#1545) 2024-12-06 10:40:45 -08:00
Chris Chávez
a0f6f4357e fix: Show published OLX in Library Content Picker [FC-0062] (#1534) 2024-12-05 14:13:04 -05:00
Braden MacDonald
e75ce15a67 refactor: Add TypeScript types for the Editors' Redux state (#1537)
It starts to add type information to the messy Redux state used by the editors, mostly focusing on the state shared by all editors and the problem editor.
2024-12-03 17:51:52 -05:00
Rômulo Penido
0771923183 feat: preserve library sidebar tab while switching items (#1535)
In the library home page, makes the selected tab on the sidebar persist while selecting Components or Collections Info Sidebar.
2024-12-03 17:13:22 -05:00
Braden MacDonald
6e53e37bfe refactor: Convert more Taxonomy code to TypeScript (2) (#1536)
* Converts some files from .js or .mjs to .ts
* Refactors some tests to use the new initializeMocks helper
* Cleans up and improves some type definitions
2024-12-02 09:24:17 -05:00
Braden MacDonald
abe68ac599 refactor: Convert more Taxonomy code to TypeScript (#1532)
* Converts some files from .js or .mjs to .ts
* Moves the API code from src/taxonomy/tag-list/data into src/taxonomy/data
* Cleans up and improves some type definitions
* No user-visible changes / functionality changes.
2024-11-27 15:31:55 -05:00
Rômulo Penido
f86c609ff1 fix: editor flicker after creating xblock (#1531) 2024-11-26 14:41:04 -08:00
Kyle McCormick
ec3f78f0ea feat!: remove Maintenance header link (#1526)
...by bumping frontend-component-header 5.7.0 -> 5.8.0

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

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

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

Related PR: https://github.com/openedx/edx-platform/pull/35852
2024-11-26 09:49:34 -05:00
Navin Karkera
55fe87a3db feat: show problem bank component picker on window msg [FC-0062] (#1522)
Fix for: If you have a unit with many components and a problem bank on the NEW MFE unit page (with an iframe), clicking "Add Components" will open a modal that's way too tall.
2024-11-22 20:29:18 +00:00
Navin Karkera
7aa5accdbb feat: preview library block changes in course unit [FC-0062] (#1506)
Creates a new preview library block modal. Intercepts the message when the block is iframed to open the new modal.
2024-11-22 15:18:43 -05:00
Diana Olarte
31f59d6bca fix: Schedule and Details page was not loading (#1527)
The page relied on obscure behavior of setting "isLibrary" to disable image uploads even in a course context. This commit refactors to use an explicit enableImageUpload prop in TinyMceWidget.
2024-11-22 12:10:11 -08:00
Peter Kulko
bc8d59b0eb chore: [FC-0070] Some tests refactoring (#1518) 2024-11-21 15:24:31 -03:00
Ihor Romaniuk
b5419acd74 feat: [FC-0070] implement move xblock modal (#1422) 2024-11-21 15:22:41 -03:00
Braden MacDonald
66577b0d59 chore: bump frontend-build, remove exceptions for prefer-default-export (#1519) 2024-11-20 09:00:02 -08:00
Chris Chávez
624f5addcf fix: Show published count component in library content picker (#1481)
When using the library component picker, show the correct number on component count (published components) in collection cards.
2024-11-19 16:10:00 -05:00
Chris Chávez
0365e3809b fix: TinyMce aux modal issues in text editors [FC-0062] (#1500)
The following bugs were found with the TinyMCE aux modal (used in emoticons, formulas and embed iframe):

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

Solution: Move the aux modal inside the editor modal.

One discarded solution: Block the modal editor from closing when interacting with the modal aux. The modal editor still retained focus.
2024-11-19 15:10:04 -05:00
Diana Olarte
b260708080 test: fix contentContainer test (#1516) 2024-11-19 12:24:32 -05:00
Diana Olarte
f740f57454 feat: upload images to v2 library components from the TinyMCE in library editor (#1458) 2024-11-18 10:38:08 -08:00
Peter Kulko
ba48a273a1 fix: fixed Configure modal for unit page (#1452) 2024-11-18 11:39:44 -03:00
Peter Kulko
0706a09acb fix: error handling (#1079)
Added alert message if 403 error occurs.
2024-11-18 11:27:36 -03:00
Régis Behmo
771c5d3e19 docs: minor README improvements (#1504) 2024-11-15 10:32:22 -08:00
Kyle McCormick
6ffdb01c24 refactor: remove pointless maintenance link message (#1503)
This link is defined in frontend-component-header, so the message
shouldn't be here. Anyway, we are deleting the link from
frontend-component-header too

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

Also, the render method was refactored to be a bit simpler.
2024-11-06 22:01:38 -05:00
Jillian
f1bdc6200f fix: show a more detailed error on Bad Request (#1468)
Show a detailed error when 400 Bad Request received while adding a component to a library, either a new or pasted component. The most likely error from the backend here is "library can only have {max} components", and since this error is translated already, we can just report it through.
2024-11-06 21:53:24 -05:00
Navin Karkera
e118eb5971 chore: hide transcripts in video preview for library (#1459)
Fixes: #1453
2024-11-05 15:15:28 -05:00
Jillian
d7bbd40de1 fix: Hide / error on Libraries v2 pages if !librariesV2Enabled (#1449)
Show an error message if the user tries to view a v2 Library while Libraries V2 are disabled in the platform.
2024-11-05 15:14:09 -05:00
edX requirements bot
fc94667a57 chore: update browserslist DB (#1381)
Co-authored-by: abdullahwaheed <42172960+abdullahwaheed@users.noreply.github.com>
2024-11-04 12:27:02 -08:00
Navin Karkera
df8a65dc4e feat: handle unsaved changes in text & problem editors (#1444)
The text & problem xblock editors will display a confirmation box before
cancelling only if user has changed something else it will directly go
back.
2024-11-04 12:41:00 -05:00
Rômulo Penido
949e4ac94c fix: enable publish button on library after component edit [FC-0062] (#1446)
Fixes the following bug: After publishing a library then editing a component, the "Publish" button in Library Info doesn't become enabled until you refresh. Updates the apiHooks to invalidates library query.
2024-11-01 13:58:39 -05:00
Navin Karkera
549dbaa0fa fix: add component to collection on paste [FC-0062] (#1450)
Link component to collection if pasted in a collection page.
Fixes: https://github.com/openedx/frontend-app-authoring/issues/1435
2024-10-31 17:44:34 -05:00
Bilal Qamar
28569aa3da test: Remove support for Node 18 (#1247)
* test: Remove support for Node 18

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

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

Co-authored-by: Farhaan Bukhsh <farhaan@opencraft.com>
2024-10-25 15:34:04 +05:30
Jillian
774728a9c0 fix: use absolute URL for Export Tags menu item (#1432)
use absolute URL for Export Tags menu item so that the menu item works no matter where in the course it's used. Fix this issue: https://github.com/openedx/frontend-app-authoring/issues/1380
2024-10-24 21:03:51 -05:00
Braden MacDonald
3d8d248599 feat: arbitrary asset upload/deletion for Library Components [FC-0062] (#1430)
Allow users to upload and delete assets associated with Content Library
components via the sidebar panel, under the "Advanced Details" section
of the "Details" tab. This is intended as a debug tool and power-user
feature, similar to the OLX editor provided there. It's also serving as
our interim image-upload solution, because it was easier to implement
than the full modal that integrates with TinyMCE.

---------

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

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

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

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

---------

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

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

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

* fix: home page initial a-z course sort

---------

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

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

* feat: manage collections

* test: add tests for manage collections

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

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

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

* feat: enable add button for blocks based on setting


---------

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

* feat: update button status on delete

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

* refactor: add an EditorContext

* test: update tests accordingly

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

use componentType or blockType instead.

* refactor: let BlockTypeLabel handle displaying the component label

including the child count, if one is provided.

This change removes hooks for the block_types REST API

* test: add tests for BlockTypeLabel

---------

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

* chore: update header and footer versions

---------

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

* feat: add comments to splice blockTypesArray code

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

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

---------

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

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

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

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

* fix: adjust margin to prevent height change

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

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

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

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

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

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

* test: update workflow test to test problem editor

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

* docs: fix comment typos found in code review

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

* fix: when deleting a fallback URL the app crashed

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

* feat: CreateCollectionModal added

* test: For CreateCollectionModal

* refactor: Migrate FormikControl to TypeScript

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

* feat: collections tab with basic cards

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

* feat: collection empty states

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

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

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

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

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

Also

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

* fix: lint issues

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

* refactor: disable discard btn for new libs

Enable it if components are added.

* refactor: invalidate library related content queries

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

* test: update snapshot

---------

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

* feat: Read only badge on library Home

* refactor: library authoring to get canEditLibrary from useContentLibrary

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

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

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

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

* fix: add image click not showing gallery

* chore: increase code coverage

* fix: empty string when no srcs need updates

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

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

---------

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

* fix: add image click not showing gallery

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

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

* feat: update asset urls without asset object

* feat: add pagination to select image modal

* fix: lint errors

* chore: update tests

* fix: asset pattern regex match

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

* fix: e.target.error for feedback fields

* fix: failing snapshots

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

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

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

* fix: increase code coverage

* fix: fix code to be more readable

* fix: delete empty file

* fix: failing test and lint

* fix: progress bar not updating

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

* fix: increase code coverage

* fix: fix code to be more readable

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

---------

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

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

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

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

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

* feat: fixed fields onblur

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

* feat: update asset urls without asset object

* feat: add pagination to select image modal

* fix: lint errors

* chore: update tests

* fix: asset pattern regex match

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

* fix: e.target.error for feedback fields

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

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

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

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

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

* fix: unlimited attempts not saving
2024-06-04 16:41:58 -04:00
Yusuf Musleh
8cde43eb5b fix: Search result redirect to unit lib component (#1027)
This change fixes redirection to the library component in the unit when selecting the search result. It also fixes an issue with navigating to the library MFE when selecting a library component.
2024-06-03 17:55:37 +05:30
Chris Chávez
460de7014e [FC-0042] Fix: Bug: Unusable "Languages" taxonomy appears in tagging drawer (#1031)
Hide language taxonomy when is empty
New message on search result when taxonomy is empty
Empty taxonomies message added in drawer
2024-06-03 17:36:54 +05:30
Kristin Aoki
9b4eb10342 fix: allow grace period minutes only (#1064)
* fix: allow grace period minutes only

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

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

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

* chore: improve code coverage

* fix: update error messages

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

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

* refactor: code refactoring

* refactor: updated tests

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

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

* feat: discussion enable setting for unit in outline

* refactor: message text

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

* fix: modal dialog overflow

---------

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

* chore: add config check

* chore: linting

* chore: variable more readable
2024-05-13 10:39:11 -04:00
Raymond Zhou
e0b70f2b17 fix: HTML screen should expand with content (#474) 2024-04-18 14:48:10 -04:00
Jesper Hodge
dedcb14386 fix: trigger pipeline release for dependency fixes (#472) 2024-04-04 13:26:46 -04:00
dependabot[bot]
cf46e6c6c9 build(deps-dev): bump follow-redirects from 1.15.5 to 1.15.6 (#471)
Bumps [follow-redirects](https://github.com/follow-redirects/follow-redirects) from 1.15.5 to 1.15.6.
- [Release notes](https://github.com/follow-redirects/follow-redirects/releases)
- [Commits](https://github.com/follow-redirects/follow-redirects/compare/v1.15.5...v1.15.6)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-04-04 13:04:17 -04:00
dependabot[bot]
77cdf2614e build(deps): bump express and @openedx/frontend-build (#470)
Bumps [express](https://github.com/expressjs/express) to 4.19.2 and updates ancestor dependency [@openedx/frontend-build](https://github.com/openedx/frontend-build). These dependencies need to be updated together.


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

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

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-04-04 13:03:56 -04:00
dependabot[bot]
fd3c871585 build(deps): bump follow-redirects from 1.15.5 to 1.15.6 in /www (#469)
Bumps [follow-redirects](https://github.com/follow-redirects/follow-redirects) from 1.15.5 to 1.15.6.
- [Release notes](https://github.com/follow-redirects/follow-redirects/releases)
- [Commits](https://github.com/follow-redirects/follow-redirects/compare/v1.15.5...v1.15.6)

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

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

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

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

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-04-04 12:52:44 -04:00
Kristin Aoki
905bea0d59 fix: default setting not updating with updated default settings (#466) 2024-03-28 15:00:16 -04:00
Kristin Aoki
4fa169556e fix: remove useEffect that fires before search params (#465)
* fix: remove useEffect that fires before search params

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

* chore: remove console logs

* fix: lint

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

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

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

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

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

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

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

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

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

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

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

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

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

* fix: video editor thumbnail fallback icon colour and size

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

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

* fix: video editor spinners vertical alignment

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

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

* fix: use trailingElement for video uploader input button

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

* docs: fix rst

* docs: add further reading section

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

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

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

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

* feat: add margin for doropdown

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

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

References
----------

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

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

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

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

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

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

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

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

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

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

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

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

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

* fix: make horizontal paddings 24px

* fix: space between widgets

* fix: remove settings heading

* fix: remove button hover effects

* fix: font size

* fix: make buttons small

* fix: change theme

* fix: reset buttons

* refactor: add Button component

* fix: hints widget

* fix: hints widget

* fix: tooltip

* fix: make settings fixed width

* fix: modal heading

* fix: center header text

* fix: modal header

* fix: settings fonts

* fix: settings fonts

* fix: fonts

* fix: padding

* fix: alignments

* fix: package.json

* fix: package.json

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

* fix: make tinymce widget look like on figma

* fix: update settingsoptions card border

* fix: header typography

* fix: spacings

* chore: update snapshots

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

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

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

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

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

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

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

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

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

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

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

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

* fix: html and react problems in problem editor

* chore: update snapshots

* chore: apply pr suggestions

* chore: fix test coverage

* chore: fix lint

* chore: fix tests

* chore: fix lint

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

* chore: add adr number

* chore: revert changes that are not docs-related

* chore: revert changes

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

* fix: problem answer layout squishes delete button background

* fix: remove borders from textarea

* fix: textarea resize

* refactor: remove renderThing-antipattern in answer option

* fix: answer option feedback color

* fix: add second feedback box to all problem types

* refactor: move extra components out of answer option file

* fix: icon disappearing on hover when active

* fix: update snapshot

* fix: lint

* fix: add tests

* fix: add tests

* fix: snapshots

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

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

* fix: resolve discussions from PR

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

* fix: lint fix

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

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

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

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

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

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

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

* feat: add code editor to problem editor

* fix: typo

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

* feat: simplify and add tests to edit problem view

* feat: add tests to problem edit view

* fix: update raw editor tests

* fix: code editor tests

* fix: package-lock

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

* fix: open the raw editor if advanced is chosen

* fix: add test fix

* feat: add button to switch visual->advanced

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

* feat: revert to advanced if parser fails

* fix: improve coverage

* feat: add confirm dialog to switch

* fix: load settings with advanced

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

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

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

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

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

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

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

* feat: add codemirror support to raw HTML editing

* feat: add test coverage

* fix: error

* fix: update codeeditor file path

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

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

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

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

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

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

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

* fix: lint

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

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

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

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

* fix: tests

* fix: remove broken code button

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

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

* feat: video skeleton

* fix: update tests

* chore: update snapshots and linting

* fix: fix image context button

* feat: re-usable editor pattern

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

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

* chore: more tests

* fix: use correct hook source for getDispatch

* fix: fix fetchImages data contract

* fix: make onClose method a callback for ImageUploadModal

* chore: lint and test updates

* chore: clean up propType warning

* fix: upload of file

* fix: error notifcations

* fix: improve test coverage

* fix: lint fixes

* fix: lint fix

* rebase: stept 1

* rebase: step 2

* fix: use correct hook source for getDispatch

* rebase: step 3

* rebase: step 4

* chore: clean up propType warning

* fix: upload of file

* rebase: step 5

* fix: improve test coverage

* fix: lint fixes

* fix: release CI

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

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

* chore: more tests

* fix: use correct hook source for getDispatch

* fix: fix fetchImages data contract

* fix: make onClose method a callback for ImageUploadModal

* chore: lint and test updates

* chore: clean up propType warning

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

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

* feat: dev gallery app

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

* feat: image settings page features

* chore: more tests

* chore: keystore util and more testing

* chore: more tests

* chore: re-install lint plugin...

* chore: lint fixes

* chore: moar tests

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

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

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

* feat: dev gallery app

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

* feat: image settings page features

* fix: update tests

* fix: console message cleanup

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

* chore: update react version

* feat: dev gallery app

* chore: fix component tests

* chore: lint fixes

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

* test: EditableHeader test ready for review 1

* test: editor header tests complete

* test: fixing up nits

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

* chore: add app thunkAction tests

* chore: resolve lint issues

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

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

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

* fix: remove typo

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

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

7
.env
View File

@@ -1,3 +1,4 @@
APP_ID='authoring'
NODE_ENV='production'
ACCESS_TOKEN_COOKIE_NAME=''
BASE_URL=''
@@ -34,12 +35,14 @@ ENABLE_UNIT_PAGE=false
ENABLE_ASSETS_PAGE=false
ENABLE_VIDEO_UPLOAD_PAGE_LINK_IN_CONTENT_DROPDOWN=false
ENABLE_TAGGING_TAXONOMY_PAGES=true
ENABLE_CERTIFICATE_PAGE=true
BBB_LEARN_MORE_URL=''
HOTJAR_APP_ID=''
HOTJAR_VERSION=6
HOTJAR_DEBUG=false
INVITE_STUDENTS_EMAIL_TO=''
AI_TRANSLATIONS_BASE_URL=''
ENABLE_HOME_PAGE_COURSE_API_V2=false
ENABLE_HOME_PAGE_COURSE_API_V2=true
ENABLE_CHECKLIST_QUALITY=''
ENABLE_GRADING_METHOD_IN_PROBLEMS=false
# "Multi-level" blocks are unsupported in libraries
LIBRARY_UNSUPPORTED_BLOCKS="conditional,step-builder,problem-builder"

View File

@@ -1,3 +1,4 @@
APP_ID='authoring'
NODE_ENV='development'
ACCESS_TOKEN_COOKIE_NAME='edx-jwt-cookie-header-payload'
BASE_URL='http://localhost:2001'
@@ -35,6 +36,7 @@ ENABLE_TEAM_TYPE_SETTING=false
ENABLE_UNIT_PAGE=false
ENABLE_ASSETS_PAGE=false
ENABLE_VIDEO_UPLOAD_PAGE_LINK_IN_CONTENT_DROPDOWN=true
ENABLE_CERTIFICATE_PAGE=true
ENABLE_NEW_VIDEO_UPLOAD_PAGE=true
ENABLE_TAGGING_TAXONOMY_PAGES=true
BBB_LEARN_MORE_URL=''
@@ -42,7 +44,8 @@ HOTJAR_APP_ID=''
HOTJAR_VERSION=6
HOTJAR_DEBUG=true
INVITE_STUDENTS_EMAIL_TO="someone@domain.com"
AI_TRANSLATIONS_BASE_URL='http://localhost:18760'
ENABLE_HOME_PAGE_COURSE_API_V2=false
ENABLE_HOME_PAGE_COURSE_API_V2=true
ENABLE_CHECKLIST_QUALITY=true
ENABLE_GRADING_METHOD_IN_PROBLEMS=false
# "Multi-level" blocks are unsupported in libraries
LIBRARY_UNSUPPORTED_BLOCKS="conditional,step-builder,problem-builder"

View File

@@ -1,3 +1,4 @@
APP_ID='authoring'
ACCESS_TOKEN_COOKIE_NAME='edx-jwt-cookie-header-payload'
BASE_URL='http://localhost:2001'
CREDENTIALS_BASE_URL='http://localhost:18150'
@@ -31,9 +32,12 @@ ENABLE_TEAM_TYPE_SETTING=false
ENABLE_UNIT_PAGE=true
ENABLE_ASSETS_PAGE=false
ENABLE_VIDEO_UPLOAD_PAGE_LINK_IN_CONTENT_DROPDOWN=true
ENABLE_CERTIFICATE_PAGE=true
ENABLE_TAGGING_TAXONOMY_PAGES=true
BBB_LEARN_MORE_URL=''
INVITE_STUDENTS_EMAIL_TO="someone@domain.com"
ENABLE_HOME_PAGE_COURSE_API_V2=true
ENABLE_CHECKLIST_QUALITY=true
ENABLE_GRADING_METHOD_IN_PROBLEMS=false
# "Multi-level" blocks are unsupported in libraries
LIBRARY_UNSUPPORTED_BLOCKS="conditional,step-builder,problem-builder"

View File

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

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

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

View File

@@ -9,14 +9,27 @@ on:
jobs:
tests:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Setup Nodejs Env
run: echo "NODE_VER=`cat .nvmrc`" >> $GITHUB_ENV
- uses: actions/setup-node@v3
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VER }}
node-version-file: '.nvmrc'
- run: make validate.ci
- name: Archive code coverage results
uses: actions/upload-artifact@v4
with:
name: code-coverage-report
path: coverage/*.*
coverage:
runs-on: ubuntu-latest
needs: tests
steps:
- uses: actions/checkout@v4
- name: Download code coverage results
uses: actions/download-artifact@v4
with:
name: code-coverage-report
- name: Upload coverage
uses: codecov/codecov-action@v4
with:

1
.gitignore vendored
View File

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

2
.nvmrc
View File

@@ -1 +1 @@
18
20

View File

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

View File

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

View File

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

View File

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

View File

@@ -4,14 +4,15 @@
apiVersion: backstage.io/v1alpha1
kind: Component
metadata:
name: 'frontend-app-course-authoring'
description: "The frontend (MFE) for Open edX Course Authoring (aka Studio)"
name: 'frontend-app-authoring'
description: "The frontend (MFE) for Open edX Authoring (aka Studio)"
links:
- url: "https://github.com/openedx/frontend-app-course-authoring"
title: "Frontend app course authoring"
- url: "https://github.com/openedx/frontend-app-authoring"
title: "Frontend app authoring"
icon: "Web"
annotations:
openedx.org/arch-interest-groups: ""
openedx.org/release: "master"
spec:
owner: group:2u-tnl
type: 'website'

View File

@@ -10,4 +10,5 @@ coverage:
threshold: 0%
ignore:
- "src/grading-settings/grading-scale/react-ranger.js"
- "src/generic/DraggableList/verticalSortableList.ts"
- "src/index.js"

View File

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

26228
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,10 +1,10 @@
{
"name": "@edx/frontend-app-course-authoring",
"name": "@edx/frontend-app-authoring",
"version": "0.1.0",
"description": "Frontend application template",
"repository": {
"type": "git",
"url": "git+https://github.com/openedx/frontend-app-course-authoring.git"
"url": "git+https://github.com/openedx/frontend-app-authoring.git"
},
"browserslist": [
"extends @edx/browserslist-config"
@@ -13,47 +13,43 @@
"build": "fedx-scripts webpack",
"i18n_extract": "fedx-scripts formatjs extract",
"stylelint": "stylelint \"plugins/**/*.scss\" \"src/**/*.scss\" \"scss/**/*.scss\" --config .stylelintrc.json",
"lint": "npm run stylelint && fedx-scripts eslint --ext .js --ext .jsx .",
"lint:fix": "npm run stylelint && fedx-scripts eslint --ext .js --ext .jsx . --fix",
"lint": "npm run stylelint && fedx-scripts eslint --ext .js --ext .jsx --ext .ts --ext .tsx .",
"lint:fix": "npm run stylelint -- --fix && fedx-scripts eslint --fix --ext .js --ext .jsx --ext .ts --ext .tsx .",
"snapshot": "TZ=UTC fedx-scripts jest --updateSnapshot",
"start": "fedx-scripts webpack-dev-server --progress",
"start:with-theme": "paragon install-theme && npm start && npm install",
"dev": "PUBLIC_PATH=/authoring/ MFE_CONFIG_API_URL='http://localhost:8000/api/mfe_config/v1' fedx-scripts webpack-dev-server --progress --host apps.local.openedx.io",
"test": "TZ=UTC fedx-scripts jest --coverage --passWithNoTests",
"test:ci": "TZ=UTC fedx-scripts jest --silent --coverage --passWithNoTests",
"types": "tsc --noEmit"
},
"husky": {
"hooks": {
"pre-commit": "npm run lint"
}
},
"author": "edX",
"license": "AGPL-3.0",
"homepage": "https://github.com/openedx/frontend-app-course-authoring#readme",
"homepage": "https://github.com/openedx/frontend-app-authoring#readme",
"publishConfig": {
"access": "public"
},
"bugs": {
"url": "https://github.com/openedx/frontend-app-course-authoring/issues"
"url": "https://github.com/openedx/frontend-app-authoring/issues"
},
"dependencies": {
"@codemirror/lang-html": "^6.0.0",
"@codemirror/lang-xml": "^6.0.0",
"@codemirror/lang-markdown": "^6.0.0",
"@codemirror/lint": "^6.2.1",
"@codemirror/state": "^6.0.0",
"@codemirror/view": "^6.0.0",
"@dnd-kit/core": "^6.1.0",
"@dnd-kit/modifiers": "^7.0.0",
"@dnd-kit/sortable": "^8.0.0",
"@dnd-kit/utilities": "^3.2.2",
"@edx/brand": "npm:@openedx/brand-openedx@^1.2.2",
"@edx/frontend-component-ai-translations": "^2.0.0",
"@edx/frontend-component-footer": "^13.0.2",
"@edx/frontend-component-header": "^5.1.0",
"@edx/frontend-enterprise-hotjar": "^2.0.0",
"@edx/frontend-lib-content-components": "^2.1.7",
"@edx/frontend-platform": "7.0.1",
"@edx/brand": "npm:@openedx/brand-openedx@^1.2.3",
"@edx/browserslist-config": "1.2.0",
"@edx/frontend-component-footer": "^14.6.0",
"@edx/frontend-component-header": "^6.2.0",
"@edx/frontend-enterprise-hotjar": "^7.2.0",
"@edx/frontend-platform": "^8.3.1",
"@edx/openedx-atlas": "^0.6.0",
"@fortawesome/fontawesome-svg-core": "1.2.36",
"@fortawesome/free-brands-svg-icons": "5.15.4",
"@fortawesome/free-regular-svg-icons": "5.15.4",
"@fortawesome/free-solid-svg-icons": "5.15.4",
"@fortawesome/react-fontawesome": "0.2.0",
"@meilisearch/instant-meilisearch": "^0.17.0",
"@openedx-plugins/course-app-calculator": "file:plugins/course-apps/calculator",
"@openedx-plugins/course-app-edxnotes": "file:plugins/course-apps/edxnotes",
"@openedx-plugins/course-app-learning_assistant": "file:plugins/course-apps/learning_assistant",
@@ -64,62 +60,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-plugin-framework": "^1.1.0",
"@openedx/paragon": "^22.2.1",
"@openedx/frontend-build": "^14.3.3",
"@openedx/frontend-plugin-framework": "^1.7.0",
"@openedx/paragon": "^22.16.0",
"@redux-devtools/extension": "^3.3.0",
"@reduxjs/toolkit": "1.9.7",
"@tanstack/react-query": "4.36.1",
"classnames": "2.2.6",
"core-js": "3.8.1",
"@tinymce/tinymce-react": "^3.14.0",
"classnames": "2.5.1",
"codemirror": "^6.0.0",
"email-validator": "2.0.4",
"fast-xml-parser": "^4.0.10",
"file-saver": "^2.0.5",
"formik": "2.2.6",
"formik": "2.4.6",
"frontend-components-tinymce-advanced-plugins": "^1.0.3",
"jszip": "^3.10.1",
"lodash": "4.17.21",
"meilisearch": "^0.38.0",
"moment": "2.29.4",
"meilisearch": "^0.41.0",
"moment": "2.30.1",
"moment-shortformat": "^2.1.0",
"npm": "^10.8.1",
"prop-types": "^15.8.1",
"react": "17.0.2",
"react": "^18.3.1",
"react-datepicker": "^4.13.0",
"react-dom": "17.0.2",
"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.16.0",
"react-router-dom": "6.16.0",
"react-router": "6.27.0",
"react-router-dom": "6.27.0",
"react-select": "5.8.0",
"react-textarea-autosize": "^8.4.1",
"react-textarea-autosize": "^8.5.3",
"react-transition-group": "4.4.5",
"redux": "4.0.5",
"regenerator-runtime": "0.13.7",
"redux-logger": "^3.0.6",
"redux-thunk": "^2.4.1",
"reselect": "^4.1.5",
"start": "^5.1.0",
"tinymce": "^5.10.4",
"universal-cookie": "^4.0.4",
"uuid": "^3.4.0",
"xmlchecker": "^0.1.0",
"yup": "0.31.1"
},
"devDependencies": {
"@edx/browserslist-config": "1.2.0",
"@edx/react-unit-test-utils": "^2.0.0",
"@edx/reactifex": "^1.0.3",
"@edx/stylelint-config-edx": "2.3.0",
"@edx/react-unit-test-utils": "^4.0.0",
"@edx/stylelint-config-edx": "2.3.3",
"@edx/typescript-config": "^1.0.1",
"@openedx/frontend-build": "13.1.0",
"@testing-library/jest-dom": "5.17.0",
"@testing-library/react": "12.1.5",
"@testing-library/react-hooks": "^8.0.1",
"@testing-library/jest-dom": "^6.6.3",
"@testing-library/react": "^16.2.0",
"@testing-library/user-event": "^13.2.1",
"axios": "^0.28.0",
"@types/lodash": "^4.17.7",
"axios-mock-adapter": "1.22.0",
"eslint-import-resolver-webpack": "^0.13.8",
"fetch-mock-jest": "^1.5.1",
"glob": "7.2.3",
"husky": "7.0.4",
"jest-canvas-mock": "^2.5.2",
"jest-expect-message": "^1.1.3",
"react-test-renderer": "17.0.2",
"reactifex": "1.1.1",
"ts-loader": "^9.5.0"
},
"peerDependencies": {
"decode-uri-component": ">=0.2.2"
"react-test-renderer": "^18.3.1",
"redux-mock-store": "^1.5.4"
}
}

View File

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

View File

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

View File

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

View File

@@ -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 { 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(() => {
@@ -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

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

@@ -2,11 +2,11 @@ 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 { SelectableBox } from '@edx/frontend-lib-content-components';
import { useIntl } from '@edx/frontend-platform/i18n';
import PropTypes from 'prop-types';
import * as Yup from 'yup';
import { useNavigate } from 'react-router-dom';
import SelectableBox from 'CourseAuthoring/editors/sharedComponents/SelectableBox';
import AppSettingsModal from 'CourseAuthoring/pages-and-resources/app-settings-modal/AppSettingsModal';
import { useModel } from 'CourseAuthoring/generic/model-store';
import Loading from 'CourseAuthoring/generic/Loading';
@@ -20,9 +20,9 @@ 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);
@@ -130,8 +130,7 @@ const LiveSettings = ({
};
LiveSettings.propTypes = {
intl: intlShape.isRequired,
onClose: PropTypes.func.isRequired,
};
export default injectIntl(LiveSettings);
export default LiveSettings;

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

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

View File

@@ -3,8 +3,7 @@
"version": "0.1.0",
"description": "Live course configuration for courses using it",
"peerDependencies": {
"@edx/frontend-app-course-authoring": "*",
"@edx/frontend-lib-content-components": "*",
"@edx/frontend-app-authoring": "*",
"@edx/frontend-platform": "*",
"@openedx/paragon": "*",
"@reduxjs/toolkit": "*",
@@ -16,7 +15,7 @@
"yup": "*"
},
"peerDependenciesMeta": {
"@edx/frontend-app-course-authoring": {
"@edx/frontend-app-authoring": {
"optional": true
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,4 +1,4 @@
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { useIntl } from '@edx/frontend-platform/i18n';
import {
ActionRow,
Alert,
@@ -70,38 +70,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 +117,11 @@ SettingsModalBase.defaultProps = {
};
const ResetUnitsButton = ({
intl,
courseId,
checked,
visible,
}) => {
const intl = useIntl();
const resetStatusRequestStatus = useSelector(getResetStatus);
const dispatch = useDispatch();
@@ -137,12 +139,12 @@ const ResetUnitsButton = ({
const getResetButtonState = () => {
switch (resetStatusRequestStatus) {
case RequestStatus.PENDING:
return 'pending';
case RequestStatus.SUCCESSFUL:
return 'finish';
default:
return 'default';
case RequestStatus.PENDING:
return 'pending';
case RequestStatus.SUCCESSFUL:
return 'finish';
default:
return 'default';
}
};
@@ -185,7 +187,6 @@ const ResetUnitsButton = ({
};
ResetUnitsButton.propTypes = {
intl: intlShape.isRequired,
courseId: PropTypes.string.isRequired,
checked: PropTypes.oneOf(['true', 'false']).isRequired,
visible: PropTypes.bool,
@@ -196,7 +197,6 @@ ResetUnitsButton.defaultProps = {
};
const SettingsModal = ({
intl,
appId,
title,
children,
@@ -213,6 +213,7 @@ const SettingsModal = ({
allUnitsEnabledText,
noUnitsEnabledText,
}) => {
const intl = useIntl();
const { courseId } = useContext(PagesAndResourcesContext);
const loadingStatus = useSelector(getLoadingStatus);
const updateSettingsRequestStatus = useSelector(getSavingStatus);
@@ -246,7 +247,7 @@ const SettingsModal = ({
success = success && await onSettingsSave(values);
}
setSaveError(!success);
!success && alertRef?.current.scrollIntoView(); // eslint-disable-line no-unused-expressions
!success && alertRef?.current.scrollIntoView(); // eslint-disable-line @typescript-eslint/no-unused-expressions
};
const handleFormikSubmit = ({ handleSubmit, errors }) => async (event) => {
@@ -372,7 +373,6 @@ const SettingsModal = ({
>
{allUnitsEnabledText}
<ResetUnitsButton
intl={intl}
courseId={courseId}
checked={formikProps.values.checked}
visible={formikProps.values.checked === 'true'}
@@ -385,7 +385,6 @@ const SettingsModal = ({
>
{noUnitsEnabledText}
<ResetUnitsButton
intl={intl}
courseId={courseId}
checked={formikProps.values.checked}
visible={formikProps.values.checked === 'false'}
@@ -423,7 +422,6 @@ const SettingsModal = ({
};
SettingsModal.propTypes = {
intl: intlShape.isRequired,
title: PropTypes.string.isRequired,
appId: PropTypes.string.isRequired,
children: PropTypes.func,
@@ -450,4 +448,4 @@ SettingsModal.defaultProps = {
enableReinitialize: false,
};
export default injectIntl(SettingsModal);
export default SettingsModal;

View File

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

View File

@@ -5,46 +5,29 @@ import { useDispatch, useSelector } from 'react-redux';
import {
useLocation,
} from 'react-router-dom';
import { StudioFooter } from '@edx/frontend-component-footer';
import { StudioFooterSlot } from '@edx/frontend-component-footer';
import Header from './header';
import { fetchCourseDetail } from './data/thunks';
import { 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';
const AppHeader = ({
courseNumber, courseOrg, courseTitle, courseId,
}) => (
<Header
courseNumber={courseNumber}
courseOrg={courseOrg}
courseTitle={courseTitle}
courseId={courseId}
/>
);
AppHeader.propTypes = {
courseId: PropTypes.string.isRequired,
courseNumber: PropTypes.string,
courseOrg: PropTypes.string,
courseTitle: PropTypes.string.isRequired,
};
AppHeader.defaultProps = {
courseNumber: null,
courseOrg: null,
};
const CourseAuthoringPage = ({ courseId, children }) => {
const dispatch = useDispatch();
useEffect(() => {
dispatch(fetchCourseDetail(courseId));
dispatch(fetchWaffleFlags(courseId));
}, [courseId]);
useEffect(() => {
dispatch(fetchOnlyStudioHomeData());
}, []);
const courseDetail = useModel('courseDetails', courseId);
const courseNumber = courseDetail ? courseDetail.number : null;
@@ -67,23 +50,23 @@ const CourseAuthoringPage = ({ courseId, children }) => {
);
}
return (
<div className={pathname.includes('/editor/') ? '' : 'bg-light-200'}>
<div>
{/* While V2 Editors are temporarily served from their own pages
using url pattern containing /editor/,
we shouldn't have the header and footer on these pages.
This functionality will be removed in TNL-9591 */}
{inProgress ? !isEditor && <Loading />
: (!isEditor && (
<AppHeader
courseNumber={courseNumber}
courseOrg={courseOrg}
courseTitle={courseTitle}
courseId={courseId}
<Header
number={courseNumber}
org={courseOrg}
title={courseTitle}
contextId={courseId}
/>
)
)}
{children}
{!inProgress && !isEditor && <StudioFooter />}
{!inProgress && !isEditor && <StudioFooterSlot />}
</div>
);
};

View File

@@ -1,18 +1,12 @@
import React from 'react';
import { getConfig } from '@edx/frontend-platform';
import { render } from '@testing-library/react';
import { getConfig, initializeMockApp } from '@edx/frontend-platform';
import MockAdapter from 'axios-mock-adapter';
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
import { AppProvider } from '@edx/frontend-platform/react';
import { IntlProvider } from '@edx/frontend-platform/i18n';
import initializeStore from './store';
import CourseAuthoringPage from './CourseAuthoringPage';
import PagesAndResources from './pages-and-resources/PagesAndResources';
import { executeThunk } from './utils';
import { fetchCourseApps } from './pages-and-resources/data/thunks';
import { fetchCourseDetail } from './data/thunks';
import { fetchCourseDetail, fetchWaffleFlags } from './data/thunks';
import { getApiWaffleFlagsUrl } from './data/api';
import { initializeMocks, render } from './testUtils';
const courseId = 'course-v1:edX+TestX+Test_Course';
let mockPathname = '/evilguy/';
@@ -25,17 +19,14 @@ jest.mock('react-router-dom', () => ({
let axiosMock;
let store;
beforeEach(() => {
initializeMockApp({
authenticatedUser: {
userId: 3,
username: 'abc123',
administrator: true,
roles: [],
},
});
store = initializeStore();
axiosMock = new MockAdapter(getAuthenticatedHttpClient());
beforeEach(async () => {
const mocks = initializeMocks();
store = mocks.reduxStore;
axiosMock = mocks.axiosMock;
axiosMock
.onGet(getApiWaffleFlagsUrl(courseId))
.reply(200, {});
await executeThunk(fetchWaffleFlags(courseId), store.dispatch);
});
describe('Editor Pages Load no header', () => {
@@ -51,13 +42,9 @@ describe('Editor Pages Load no header', () => {
mockPathname = '/editor/';
await mockStoreSuccess();
const wrapper = render(
<AppProvider store={store}>
<IntlProvider locale="en">
<CourseAuthoringPage courseId={courseId}>
<PagesAndResources courseId={courseId} />
</CourseAuthoringPage>
</IntlProvider>
</AppProvider>
<CourseAuthoringPage courseId={courseId}>
<PagesAndResources courseId={courseId} />
</CourseAuthoringPage>
,
);
expect(wrapper.queryByRole('status')).not.toBeInTheDocument();
@@ -66,13 +53,9 @@ describe('Editor Pages Load no header', () => {
mockPathname = '/evilguy/';
await mockStoreSuccess();
const wrapper = render(
<AppProvider store={store}>
<IntlProvider locale="en">
<CourseAuthoringPage courseId={courseId}>
<PagesAndResources courseId={courseId} />
</CourseAuthoringPage>
</IntlProvider>
</AppProvider>
<CourseAuthoringPage courseId={courseId}>
<PagesAndResources courseId={courseId} />
</CourseAuthoringPage>
,
);
expect(wrapper.queryByRole('status')).toBeInTheDocument();
@@ -100,14 +83,7 @@ describe('Course authoring page', () => {
};
test('renders not found page on non-existent course key', async () => {
await mockStoreNotFound();
const wrapper = render(
<AppProvider store={store}>
<IntlProvider locale="en">
<CourseAuthoringPage courseId={courseId} />
</IntlProvider>
</AppProvider>
,
);
const wrapper = render(<CourseAuthoringPage courseId={courseId} />);
expect(await wrapper.findByTestId('notFoundAlert')).toBeInTheDocument();
});
test('does not render not found page on other kinds of error', async () => {
@@ -118,13 +94,9 @@ describe('Course authoring page', () => {
// found alert is not present.
const contentTestId = 'courseAuthoringPageContent';
const wrapper = render(
<AppProvider store={store}>
<IntlProvider locale="en">
<CourseAuthoringPage courseId={courseId}>
<div data-testid={contentTestId} />
</CourseAuthoringPage>
</IntlProvider>
</AppProvider>
<CourseAuthoringPage courseId={courseId}>
<div data-testid={contentTestId} />
</CourseAuthoringPage>
,
);
expect(await wrapper.findByTestId(contentTestId)).toBeInTheDocument();

View File

@@ -4,7 +4,7 @@ import {
} from 'react-router-dom';
import { getConfig } from '@edx/frontend-platform';
import { PageWrap } from '@edx/frontend-platform/react';
import { Textbooks } from 'CourseAuthoring/textbooks';
import { Textbooks } from './textbooks';
import CourseAuthoringPage from './CourseAuthoringPage';
import { PagesAndResources } from './pages-and-resources';
import EditorContainer from './editors/EditorContainer';
@@ -20,10 +20,13 @@ 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:
@@ -55,6 +58,10 @@ const CourseAuthoringRoutes = () => {
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>}
@@ -79,7 +86,7 @@ const CourseAuthoringRoutes = () => {
<Route
key={path}
path={path}
element={<PageWrap><CourseUnit courseId={courseId} /></PageWrap>}
element={<PageWrap><IframeProvider><CourseUnit courseId={courseId} /></IframeProvider></PageWrap>}
/>
))}
<Route
@@ -88,7 +95,7 @@ const CourseAuthoringRoutes = () => {
/>
<Route
path="editor/:blockType/:blockId?"
element={<PageWrap><EditorContainer courseId={courseId} /></PageWrap>}
element={<PageWrap><EditorContainer learningContextId={courseId} /></PageWrap>}
/>
<Route
path="settings/details"
@@ -118,13 +125,17 @@ const CourseAuthoringRoutes = () => {
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={<PageWrap><Certificates courseId={courseId} /></PageWrap>}
element={getConfig().ENABLE_CERTIFICATE_PAGE === 'true' ? <PageWrap><Certificates courseId={courseId} /></PageWrap> : null}
/>
<Route
path="textbooks"

View File

@@ -1,10 +1,10 @@
import React from 'react';
import { AppProvider } from '@edx/frontend-platform/react';
import { initializeMockApp } from '@edx/frontend-platform';
import { render, screen } from '@testing-library/react';
import { MemoryRouter } from 'react-router-dom';
import CourseAuthoringRoutes from './CourseAuthoringRoutes';
import initializeStore from './store';
import { executeThunk } from './utils';
import { getApiWaffleFlagsUrl } from './data/api';
import { fetchWaffleFlags } from './data/thunks';
import {
screen, initializeMocks, render, waitFor,
} from './testUtils';
const courseId = 'course-v1:edX+TestX+Test_Course';
const pagesAndResourcesMockText = 'Pages And Resources';
@@ -21,9 +21,10 @@ jest.mock('react-router-dom', () => ({
}),
}));
// Mock the TinyMceWidget from frontend-lib-content-components
jest.mock('@edx/frontend-lib-content-components', () => ({
TinyMceWidget: () => <div>Widget</div>,
// Mock the TinyMceWidget
jest.mock('./editors/sharedComponents/TinyMceWidget', () => ({
__esModule: true, // Required to mock a default export
default: () => <div>Widget</div>,
Footer: () => <div>Footer</div>,
prepareEditorRef: jest.fn(() => ({
refReady: true,
@@ -49,68 +50,59 @@ jest.mock('./custom-pages/CustomPages', () => (props) => {
});
describe('<CourseAuthoringRoutes>', () => {
beforeEach(() => {
initializeMockApp({
authenticatedUser: {
userId: 3,
username: 'abc123',
administrator: true,
roles: [],
},
beforeEach(async () => {
const { axiosMock, reduxStore } = initializeMocks();
store = reduxStore;
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 () => {
render(
<CourseAuthoringRoutes />,
{ routerProps: { initialEntries: ['/pages-and-resources'] } },
);
await waitFor(() => {
expect(screen.getByText(pagesAndResourcesMockText)).toBeVisible();
expect(mockComponentFn).toHaveBeenCalledWith(
expect.objectContaining({
courseId,
}),
);
});
store = initializeStore();
});
fit('renders the PagesAndResources component when the pages and resources route is active', () => {
it('renders the EditorContainer component when the course editor route is active', async () => {
render(
<AppProvider store={store} wrapWithRouter={false}>
<MemoryRouter initialEntries={['/pages-and-resources']}>
<CourseAuthoringRoutes />
</MemoryRouter>
</AppProvider>,
);
expect(screen.getByText(pagesAndResourcesMockText)).toBeVisible();
expect(mockComponentFn).toHaveBeenCalledWith(
expect.objectContaining({
courseId,
}),
<CourseAuthoringRoutes />,
{ routerProps: { initialEntries: ['/editor/video/block-id'] } },
);
await waitFor(() => {
expect(screen.queryByText(editorContainerMockText)).toBeInTheDocument();
expect(screen.queryByText(pagesAndResourcesMockText)).not.toBeInTheDocument();
expect(mockComponentFn).toHaveBeenCalledWith(
expect.objectContaining({
learningContextId: courseId,
}),
);
});
});
it('renders the EditorContainer component when the course editor route is active', () => {
it('renders the VideoSelectorContainer component when the course videos route is active', async () => {
render(
<AppProvider store={store} wrapWithRouter={false}>
<MemoryRouter initialEntries={['/editor/video/block-id']}>
<CourseAuthoringRoutes />
</MemoryRouter>
</AppProvider>,
);
expect(screen.queryByText(editorContainerMockText)).toBeInTheDocument();
expect(screen.queryByText(pagesAndResourcesMockText)).not.toBeInTheDocument();
expect(mockComponentFn).toHaveBeenCalledWith(
expect.objectContaining({
courseId,
}),
);
});
it('renders the VideoSelectorContainer component when the course videos route is active', () => {
render(
<AppProvider store={store} wrapWithRouter={false}>
<MemoryRouter initialEntries={['/editor/course-videos/block-id']}>
<CourseAuthoringRoutes />
</MemoryRouter>
</AppProvider>,
);
expect(screen.queryByText(videoSelectorContainerMockText)).toBeInTheDocument();
expect(screen.queryByText(pagesAndResourcesMockText)).not.toBeInTheDocument();
expect(mockComponentFn).toHaveBeenCalledWith(
expect.objectContaining({
courseId,
}),
<CourseAuthoringRoutes />,
{ routerProps: { initialEntries: ['/editor/course-videos/block-id'] } },
);
await waitFor(() => {
expect(screen.queryByText(videoSelectorContainerMockText)).toBeInTheDocument();
expect(screen.queryByText(pagesAndResourcesMockText)).not.toBeInTheDocument();
expect(mockComponentFn).toHaveBeenCalledWith(
expect.objectContaining({
courseId,
}),
);
});
});
});

View File

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

View File

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

View File

@@ -2,7 +2,7 @@ import React from 'react';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { Helmet } from 'react-helmet';
import { Container } from '@openedx/paragon';
import { StudioFooter } from '@edx/frontend-component-footer';
import { StudioFooterSlot } from '@edx/frontend-component-footer';
import Header from '../header';
import messages from './messages';
@@ -29,7 +29,7 @@ const AccessibilityPage = ({
<AccessibilityBody {...{ email, communityAccessibilityLink }} />
<AccessibilityForm accessibilityEmail={email} />
</Container>
<StudioFooter />
<StudioFooterSlot />
</>
);
};

View File

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

View File

@@ -6,7 +6,7 @@ import {
} from '@openedx/paragon';
import { CheckCircle, Info, Warning } from '@openedx/paragon/icons';
import { FormattedMessage, injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import Placeholder from '@edx/frontend-lib-content-components';
import Placeholder from '../editors/Placeholder';
import AlertProctoringError from '../generic/AlertProctoringError';
import { useModel } from '../generic/model-store';

View File

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

View File

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

View File

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

View File

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

View File

@@ -71,7 +71,7 @@ const SettingCard = ({
iconAs={Icon}
alt={intl.formatMessage(messages.helpButtonText)}
variant="primary"
className=" ml-1 mr-2"
className="flex-shrink-0 ml-1 mr-2"
/>
<ModalPopup
hasArrow

View File

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

View File

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

View File

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

View File

@@ -1,6 +1,5 @@
import { v4 as uuid } from 'uuid';
// eslint-disable-next-line import/prefer-default-export
export const defaultCertificate = {
courseTitle: '',
signatories: [{

View File

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

View File

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

View File

@@ -1,4 +1,3 @@
// eslint-disable-next-line import/prefer-default-export
export const getSidebarData = ({ messages, intl }) => [
{
title: intl.formatMessage(messages.workingWithCertificatesTitle),

View File

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

View File

@@ -1,6 +1,5 @@
import { convertObjectToSnakeCase } from '../utils';
// eslint-disable-next-line import/prefer-default-export
export const prepareCertificatePayload = (data) => convertObjectToSnakeCase(({
...data,
courseTitle: data.courseTitle,

View File

@@ -27,6 +27,8 @@ export const NOTIFICATION_MESSAGES = {
copying: 'Copying',
pasting: 'Pasting',
discardChanges: 'Discarding changes',
moving: 'Moving',
undoMoving: 'Undo moving',
publishing: 'Publishing',
hidingFromStudents: 'Hiding from students',
makingVisibleToStudents: 'Making visible to students',
@@ -56,6 +58,8 @@ export const COURSE_BLOCK_NAMES = ({
chapter: { id: 'chapter', name: 'Section' },
sequential: { id: 'sequential', name: 'Subsection' },
vertical: { id: 'vertical', name: 'Unit' },
libraryContent: { id: 'library_content', name: 'Library content' },
splitTest: { id: 'split_test', name: 'Split Test' },
component: { id: 'component', name: 'Component' },
});
@@ -69,3 +73,36 @@ export const CLIPBOARD_STATUS = {
};
export const STRUCTURAL_XBLOCK_TYPES = ['vertical', 'sequential', 'chapter', 'course'];
export const REGEX_RULES = {
specialCharsRule: /^[a-zA-Z0-9_\-.'*~\s]+$/,
noSpaceRule: /^\S*$/,
};
/**
* Feature policy for iframe, allowing access to certain courseware-related media.
*
* We must use the wildcard (*) origin for each feature, as courseware content
* may be embedded in external iframes. Notably, xblock-lti-consumer is a popular
* block that iframes external course content.
* This policy was selected in conference with the edX Security Working Group.
* Changes to it should be vetted by them (security@edx.org).
*/
export const IFRAME_FEATURE_POLICY = (
'microphone *; camera *; midi *; geolocation *; encrypted-media *; clipboard-write *'
);
export const iframeStateKeys = {
iframeHeight: 'iframeHeight',
hasLoaded: 'hasLoaded',
showError: 'showError',
windowTopOffset: 'windowTopOffset',
};
export const iframeMessageTypes = {
modal: 'plugin.modal',
resize: 'plugin.resize',
videoFullScreen: 'plugin.videoFullScreen',
xblockEvent: 'xblock-event',
};

View File

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

View File

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

View File

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

View File

@@ -6,11 +6,11 @@ import { cloneDeep } from 'lodash';
import { useContentTaxonomyTagsUpdater } from './data/apiHooks';
import { ContentTagsDrawerContext } from './common/context';
/** @typedef {import("../taxonomy/data/types.mjs").TaxonomyData} TaxonomyData */
/** @typedef {import("./data/types.mjs").Tag} ContentTagData */
/** @typedef {import("../taxonomy/data/types.js").TaxonomyData} TaxonomyData */
/** @typedef {import("./data/types.js").Tag} ContentTagData */
/** @typedef {import("./ContentTagsCollapsible").TagTreeEntry} TagTreeEntry */
/** @typedef {import("./data/types.mjs").StagedTagData} StagedTagData */
/** @typedef {import("./data/types.mjs").UpdateTagsData} UpdateTagsData */
/** @typedef {import("./data/types.js").StagedTagData} StagedTagData */
/** @typedef {import("./data/types.js").UpdateTagsData} UpdateTagsData */
/**
* Util function that sorts the keys of a tree in alphabetical order.
@@ -116,7 +116,7 @@ const useContentTagsCollapsibleHelper = (
// State to keep track of the staged tags (and along with ancestors) that should be removed
const [stagedTagsToRemove, setStagedTagsToRemove] = React.useState(/** @type string[] */([]));
// State to keep track of the global tags (stagged and feched) that should be removed
// State to keep track of the global tags (staged and fetched) that should be removed
const [globalTagsToRemove, setGlobalTagsToRemove] = React.useState(/** @type string[] */([]));
// Handles the removal of staged content tags based on what was removed
@@ -140,7 +140,7 @@ const useContentTagsCollapsibleHelper = (
// A new tag has been removed
removeGlobalStagedContentTag(id, tag);
} else if (contentTags.some(t => t.value === tag)) {
// A feched tag has been removed
// A fetched tag has been removed
addRemovedContentTag(id, tag);
}
});
@@ -157,7 +157,7 @@ const useContentTagsCollapsibleHelper = (
explicitStaged.forEach((tag) => {
if (globalStagedRemovedContentTags[id]
&& globalStagedRemovedContentTags[id].includes(tag.value)) {
// A feched tag that has been removed has been added again
// A fetched tag that has been removed has been added again
deleteRemovedContentTag(id, tag.value);
} else {
// New tag added
@@ -298,7 +298,7 @@ const useContentTagsCollapsibleHelper = (
traversal[tag].lineage = tagLineage;
}
// eslint-disable-next-line no-unused-expressions
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
isExplicit ? add(value.join(',')) : remove(value.join(','));
traversal = traversal[tag].children;
});

View File

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

View File

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

View File

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

View File

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

View File

@@ -4,49 +4,27 @@ import { useIntl } from '@edx/frontend-platform/i18n';
import { cloneDeep } from 'lodash';
import { useContentData, useContentTaxonomyTagsData, useContentTaxonomyTagsUpdater } from './data/apiHooks';
import { useTaxonomyList } from '../taxonomy/data/apiHooks';
import { extractOrgFromContentId } from './utils';
import { extractOrgFromContentId, languageExportId } from './utils';
import messages from './messages';
import { ContentTagsDrawerSheetContext } from './common/context';
/** @typedef {import("./data/types.mjs").Tag} ContentTagData */
/** @typedef {import("./data/types.mjs").StagedTagData} StagedTagData */
/** @typedef {import("./data/types.mjs").TagsInTaxonomy} TagsInTaxonomy */
/** @typedef {import("./data/types.js").Tag} ContentTagData */
/** @typedef {import("./data/types.js").StagedTagData} StagedTagData */
/** @typedef {import("./data/types.js").TagsInTaxonomy} TagsInTaxonomy */
/** @typedef {import("./common/context").ContentTagsDrawerContextData} ContentTagsDrawerContextData */
/**
* Handles the context and all the underlying logic for the ContentTagsDrawer component
* Helper hook for *creating* a `ContentTagsDrawerContext`.
* Handles the context and all the underlying logic for the ContentTagsDrawer component.
*
* To *use* the context, just use `useContext(ContentTagsDrawerContext)`
* @param {string} contentId
* @returns {{
* stagedContentTags: Record<number, StagedTagData[]>,
* addStagedContentTag: (taxonomyId: number, addedTag: StagedTagData) => void,
* removeStagedContentTag: (taxonomyId: number, tagValue: string) => void,
* removeGlobalStagedContentTag: (taxonomyId: number, tagValue: string) => void,
* addRemovedContentTag: (taxonomyId: number, tagValue: string) => void,
* deleteRemovedContentTag: (taxonomyId: number, tagValue: string) => void,
* setStagedTags: (taxonomyId: number, tagsList: StagedTagData[]) => void,
* globalStagedContentTags: Record<number, StagedTagData[]>,
* globalStagedRemovedContentTags: Record<number, string>,
* setGlobalStagedContentTags: Function,
* commitGlobalStagedTags: () => void,
* commitGlobalStagedTagsStatus: string,
* isContentDataLoaded: boolean,
* isContentTaxonomyTagsLoaded: boolean,
* isTaxonomyListLoaded: boolean,
* contentName: string,
* tagsByTaxonomy: TagsInTaxonomy[],
* isEditMode: boolean,
* toEditMode: () => void,
* toReadMode: () => void,
* collapsibleStates: Record<number, boolean>,
* openCollapsible: (taxonomyId: number) => void,
* closeCollapsible: (taxonomyId: number) => void,
* toastMessage: string | undefined,
* showToastAfterSave: () => void,
* closeToast: () => void,
* setCollapsibleToInitalState: () => void,
* otherTaxonomies: TagsInTaxonomy[],
* }}
* @param {boolean} canTagObject
* @param {boolean} fetchMetadata=false If true, fetches metadata for the contentId. This is used on `edx-platform`
* and the Course/Unit Outline to show the content name as the drawer title.
* @returns {ContentTagsDrawerContextData}
*/
const useContentTagsDrawerContext = (contentId) => {
export const useCreateContentTagsDrawerContext = (contentId, canTagObject, fetchMetadata = false) => {
const intl = useIntl();
const org = extractOrgFromContentId(contentId);
@@ -58,9 +36,9 @@ const useContentTagsDrawerContext = (contentId) => {
const [stagedContentTags, setStagedContentTags] = React.useState({});
// When a staged tags on a taxonomy is commitet then is saved on this map.
const [globalStagedContentTags, setGlobalStagedContentTags] = React.useState({});
// This stores feched tags deleted by the user.
// This stores fetched tags deleted by the user.
const [globalStagedRemovedContentTags, setGlobalStagedRemovedContentTags] = React.useState({});
// Merges feched tags, global staged tags and global removed staged tags
// Merges fetched tags, global staged tags and global removed staged tags
const [tagsByTaxonomy, setTagsByTaxonomy] = React.useState(/** @type TagsInTaxonomy[] */ ([]));
// Other taxonomies that the user doesn't have permissions
const [otherTaxonomies, setOtherTaxonomies] = React.useState(/** @type TagsInTaxonomy[] */ ([]));
@@ -72,15 +50,15 @@ const useContentTagsDrawerContext = (contentId) => {
const updateTags = useContentTaxonomyTagsUpdater(contentId);
// Fetch from database
const { data: contentData, isSuccess: isContentDataLoaded } = useContentData(contentId);
const { data: contentData, isSuccess: isContentDataLoaded } = useContentData(contentId, fetchMetadata);
const {
data: contentTaxonomyTagsData,
isSuccess: isContentTaxonomyTagsLoaded,
} = useContentTaxonomyTagsData(contentId);
const { data: taxonomyListData, isSuccess: isTaxonomyListLoaded } = useTaxonomyList(org);
// Tags feched from database
const { fechedTaxonomies, fechedOtherTaxonomies } = React.useMemo(() => {
// Tags fetched from database
const { fetchedTaxonomies, fetchedOtherTaxonomies } = React.useMemo(() => {
const sortTaxonomies = (taxonomiesList) => {
const taxonomiesWithData = taxonomiesList.filter(
(t) => t.contentTags.length !== 0,
@@ -115,6 +93,7 @@ const useContentTagsDrawerContext = (contentId) => {
// Initialize list of content tags in taxonomies to populate
const taxonomiesList = taxonomyListData.results.map((taxonomy) => ({
...taxonomy,
canTagObject: taxonomy.canTagObject && canTagObject,
contentTags: /** @type {ContentTagData[]} */([]),
}));
@@ -142,14 +121,20 @@ const useContentTagsDrawerContext = (contentId) => {
}
});
// Delete Language taxonomy if is empty
const filteredTaxonomies = taxonomiesList.filter(
(taxonomy) => taxonomy.exportId !== languageExportId
|| taxonomy.contentTags.length !== 0,
);
return {
fechedTaxonomies: sortTaxonomies(taxonomiesList),
fechedOtherTaxonomies: otherTaxonomiesList,
fetchedTaxonomies: sortTaxonomies(filteredTaxonomies),
fetchedOtherTaxonomies: otherTaxonomiesList,
};
}
return {
fechedTaxonomies: [],
fechedOtherTaxonomies: [],
fetchedTaxonomies: [],
fetchedOtherTaxonomies: [],
};
}, [taxonomyListData, contentTaxonomyTagsData]);
@@ -224,28 +209,28 @@ const useContentTagsDrawerContext = (contentId) => {
const openAllCollapsible = React.useCallback(() => {
const updatedState = {};
fechedTaxonomies.forEach((taxonomy) => {
fetchedTaxonomies.forEach((taxonomy) => {
updatedState[taxonomy.id] = true;
});
fechedOtherTaxonomies.forEach((taxonomy) => {
fetchedOtherTaxonomies.forEach((taxonomy) => {
updatedState[taxonomy.id] = true;
});
setColapsibleStates(updatedState);
}, [fechedTaxonomies, setColapsibleStates]);
}, [fetchedTaxonomies, setColapsibleStates]);
// Set initial state of collapsible based on content tags
const setCollapsibleToInitalState = React.useCallback(() => {
const updatedState = {};
fechedTaxonomies.forEach((taxonomy) => {
fetchedTaxonomies.forEach((taxonomy) => {
// Taxonomy with content tags must be open
updatedState[taxonomy.id] = taxonomy.contentTags.length !== 0;
});
fechedOtherTaxonomies.forEach((taxonomy) => {
fetchedOtherTaxonomies.forEach((taxonomy) => {
// Taxonomy with content tags must be open
updatedState[taxonomy.id] = taxonomy.contentTags.length !== 0;
});
setColapsibleStates(updatedState);
}, [fechedTaxonomies, setColapsibleStates]);
}, [fetchedTaxonomies, setColapsibleStates]);
// Changes the drawer mode to edit
const toEditMode = React.useCallback(() => {
@@ -325,7 +310,7 @@ const useContentTagsDrawerContext = (contentId) => {
const closeToast = React.useCallback(() => setToastMessage(undefined), [setToastMessage]);
let contentName = '';
if (isContentDataLoaded) {
if (isContentDataLoaded && contentData) {
if ('displayName' in contentData) {
contentName = contentData.displayName;
} else {
@@ -333,14 +318,14 @@ const useContentTagsDrawerContext = (contentId) => {
}
}
// Updates `tagsByTaxonomy` merged feched tags, global staged tags
// Updates `tagsByTaxonomy` merged fetched tags, global staged tags
// and global removed staged tags.
React.useEffect(() => {
const mergedTags = cloneDeep(fechedTaxonomies).reduce((acc, obj) => (
const mergedTags = cloneDeep(fetchedTaxonomies).reduce((acc, obj) => (
{ ...acc, [obj.id]: obj }
), {});
const mergedOtherTaxonomies = cloneDeep(fechedOtherTaxonomies).reduce((acc, obj) => (
const mergedOtherTaxonomies = cloneDeep(fetchedOtherTaxonomies).reduce((acc, obj) => (
{ ...acc, [obj.id]: obj }
), {});
@@ -349,10 +334,10 @@ const useContentTagsDrawerContext = (contentId) => {
// TODO test this
// Filter out applied tags that should become implicit because a child tag was committed
const stagedLineages = globalStagedContentTags[taxonomyId].map((t) => t.lineage.slice(0, -1)).flat();
const fechedTags = mergedTags[taxonomyId].contentTags.filter((t) => !stagedLineages.includes(t.value));
const fetchedTags = mergedTags[taxonomyId].contentTags.filter((t) => !stagedLineages.includes(t.value));
mergedTags[taxonomyId].contentTags = [
...fechedTags,
...fetchedTags,
...globalStagedContentTags[taxonomyId],
];
}
@@ -371,8 +356,8 @@ const useContentTagsDrawerContext = (contentId) => {
});
// It is constructed this way to maintain the order
// of the list `fechedTaxonomies`
const mergedTagsArray = fechedTaxonomies.map(obj => mergedTags[obj.id]);
// of the list `fetchedTaxonomies`
const mergedTagsArray = fetchedTaxonomies.map(obj => mergedTags[obj.id]);
setTagsByTaxonomy(mergedTagsArray);
setOtherTaxonomies(Object.values(mergedOtherTaxonomies));
@@ -402,8 +387,8 @@ const useContentTagsDrawerContext = (contentId) => {
}
}
}, [
fechedTaxonomies,
fechedOtherTaxonomies,
fetchedTaxonomies,
fetchedOtherTaxonomies,
globalStagedContentTags,
globalStagedRemovedContentTags,
]);
@@ -457,5 +442,3 @@ const useContentTagsDrawerContext = (contentId) => {
otherTaxonomies,
};
};
export default useContentTagsDrawerContext;

View File

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

View File

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

View File

@@ -1,7 +1,6 @@
import React from 'react';
import { IntlProvider } from '@edx/frontend-platform/i18n';
import {
act,
render,
waitFor,
fireEvent,
@@ -74,11 +73,9 @@ describe('<ContentTagsDropDownSelector />', () => {
}
it('should render taxonomy tags drop down selector loading with spinner', async () => {
await act(async () => {
const { getByRole } = await getComponent();
const spinner = getByRole('status');
expect(spinner.textContent).toEqual('Loading tags'); // Uses <Spinner />
});
const { getByRole } = await getComponent();
const spinner = getByRole('status');
expect(spinner.textContent).toEqual('Loading tags'); // Uses <Spinner />
});
it('should render taxonomy tags drop down selector with no sub tags', async () => {
@@ -99,13 +96,11 @@ describe('<ContentTagsDropDownSelector />', () => {
},
});
await act(async () => {
const { container, getByText } = await getComponent();
const { container, getByText } = await getComponent();
await waitFor(() => {
expect(getByText('Tag 1')).toBeInTheDocument();
expect(container.getElementsByClassName('taxonomy-tags-arrow-drop-down').length).toBe(0);
});
await waitFor(() => {
expect(getByText('Tag 1')).toBeInTheDocument();
expect(container.getElementsByClassName('taxonomy-tags-arrow-drop-down').length).toBe(0);
});
});
@@ -127,13 +122,11 @@ describe('<ContentTagsDropDownSelector />', () => {
},
});
await act(async () => {
const { container, getByText } = await getComponent();
const { container, getByText } = await getComponent();
await waitFor(() => {
expect(getByText('Tag 2')).toBeInTheDocument();
expect(container.getElementsByClassName('taxonomy-tags-arrow-drop-down').length).toBe(1);
});
await waitFor(() => {
expect(getByText('Tag 2')).toBeInTheDocument();
expect(container.getElementsByClassName('taxonomy-tags-arrow-drop-down').length).toBe(1);
});
});
@@ -155,47 +148,45 @@ describe('<ContentTagsDropDownSelector />', () => {
},
});
await act(async () => {
const dataWithTagsTree = {
...data,
tagsTree: {
'Tag 3': {
explicit: false,
children: {},
},
const dataWithTagsTree = {
...data,
tagsTree: {
'Tag 3': {
explicit: false,
children: {},
},
};
const { container, getByText } = await getComponent(dataWithTagsTree);
await waitFor(() => {
expect(getByText('Tag 2')).toBeInTheDocument();
expect(container.getElementsByClassName('taxonomy-tags-arrow-drop-down').length).toBe(1);
});
},
};
const { container, getByText } = await getComponent(dataWithTagsTree);
await waitFor(() => {
expect(getByText('Tag 2')).toBeInTheDocument();
expect(container.getElementsByClassName('taxonomy-tags-arrow-drop-down').length).toBe(1);
});
// Mock useTaxonomyTagsData again since it gets called in the recursive call
useTaxonomyTagsData.mockReturnValueOnce({
hasMorePages: false,
tagPages: {
isLoading: false,
isError: false,
data: [{
value: 'Tag 3',
externalId: null,
childCount: 0,
depth: 1,
parentValue: 'Tag 2',
id: 12346,
subTagsUrl: null,
}],
},
});
// Mock useTaxonomyTagsData again since it gets called in the recursive call
useTaxonomyTagsData.mockReturnValueOnce({
hasMorePages: false,
tagPages: {
isLoading: false,
isError: false,
data: [{
value: 'Tag 3',
externalId: null,
childCount: 0,
depth: 1,
parentValue: 'Tag 2',
id: 12346,
subTagsUrl: null,
}],
},
});
// Expand the dropdown to see the subtags selectors
const expandToggle = container.querySelector('.taxonomy-tags-arrow-drop-down span');
fireEvent.click(expandToggle);
// Expand the dropdown to see the subtags selectors
const expandToggle = container.querySelector('.taxonomy-tags-arrow-drop-down span');
fireEvent.click(expandToggle);
await waitFor(() => {
expect(getByText('Tag 3')).toBeInTheDocument();
});
await waitFor(() => {
expect(getByText('Tag 3')).toBeInTheDocument();
});
});
@@ -219,48 +210,46 @@ describe('<ContentTagsDropDownSelector />', () => {
});
const initalSearchTerm = 'test 1';
await act(async () => {
const { rerender } = await getComponent({ ...data, searchTerm: initalSearchTerm });
const { rerender } = await getComponent({ ...data, searchTerm: initalSearchTerm });
await waitFor(() => {
expect(useTaxonomyTagsData).toBeCalledWith(data.taxonomyId, null, 1, initalSearchTerm);
});
await waitFor(() => {
expect(useTaxonomyTagsData).toHaveBeenCalledWith(data.taxonomyId, null, 1, initalSearchTerm);
});
const updatedSearchTerm = 'test 2';
rerender(<ContentTagsDropDownSelectorComponent
key={`selector-${data.taxonomyId}`}
taxonomyId={data.taxonomyId}
level={data.level}
tagsTree={data.tagsTree}
searchTerm={updatedSearchTerm}
appliedContentTagsTree={{}}
stagedContentTagsTree={{}}
/>);
const updatedSearchTerm = 'test 2';
rerender(<ContentTagsDropDownSelectorComponent
key={`selector-${data.taxonomyId}`}
taxonomyId={data.taxonomyId}
level={data.level}
tagsTree={data.tagsTree}
searchTerm={updatedSearchTerm}
appliedContentTagsTree={{}}
stagedContentTagsTree={{}}
/>);
await waitFor(() => {
expect(useTaxonomyTagsData).toBeCalledWith(data.taxonomyId, null, 1, updatedSearchTerm);
});
await waitFor(() => {
expect(useTaxonomyTagsData).toHaveBeenCalledWith(data.taxonomyId, null, 1, updatedSearchTerm);
});
// Clean search term
const cleanSearchTerm = '';
rerender(<ContentTagsDropDownSelectorComponent
key={`selector-${data.taxonomyId}`}
taxonomyId={data.taxonomyId}
level={data.level}
tagsTree={data.tagsTree}
searchTerm={cleanSearchTerm}
appliedContentTagsTree={{}}
stagedContentTagsTree={{}}
/>);
// Clean search term
const cleanSearchTerm = '';
rerender(<ContentTagsDropDownSelectorComponent
key={`selector-${data.taxonomyId}`}
taxonomyId={data.taxonomyId}
level={data.level}
tagsTree={data.tagsTree}
searchTerm={cleanSearchTerm}
appliedContentTagsTree={{}}
stagedContentTagsTree={{}}
/>);
await waitFor(() => {
expect(useTaxonomyTagsData).toBeCalledWith(data.taxonomyId, null, 1, cleanSearchTerm);
});
await waitFor(() => {
expect(useTaxonomyTagsData).toHaveBeenCalledWith(data.taxonomyId, null, 1, cleanSearchTerm);
});
});
it('should render "noTag" message if search doesnt return taxonomies', async () => {
useTaxonomyTagsData.mockReturnValueOnce({
useTaxonomyTagsData.mockReturnValue({
hasMorePages: false,
tagPages: {
isLoading: false,
@@ -271,15 +260,35 @@ describe('<ContentTagsDropDownSelector />', () => {
});
const searchTerm = 'uncommon search term';
await act(async () => {
const { getByText } = await getComponent({ ...data, searchTerm });
const { getByText } = await getComponent({ ...data, searchTerm });
await waitFor(() => {
expect(useTaxonomyTagsData).toBeCalledWith(data.taxonomyId, null, 1, searchTerm);
});
const message = `No tags found with the search term "${searchTerm}"`;
expect(getByText(message)).toBeInTheDocument();
await waitFor(() => {
expect(useTaxonomyTagsData).toHaveBeenCalledWith(data.taxonomyId, null, 1, searchTerm);
});
const message = `No tags found with the search term "${searchTerm}"`;
expect(getByText(message)).toBeInTheDocument();
});
it('should render "noTagsInTaxonomy" message if taxonomy is empty', async () => {
useTaxonomyTagsData.mockReturnValue({
hasMorePages: false,
tagPages: {
isLoading: false,
isError: false,
isSuccess: true,
data: [],
},
});
const searchTerm = '';
const { getByText } = await getComponent({ ...data, searchTerm });
await waitFor(() => {
expect(useTaxonomyTagsData).toHaveBeenCalledWith(data.taxonomyId, null, 1, searchTerm);
});
const message = 'No tags in this taxonomy yet';
expect(getByText(message)).toBeInTheDocument();
});
});

View File

@@ -1,49 +0,0 @@
// @ts-check
/* eslint-disable import/prefer-default-export */
import React from 'react';
/** @typedef {import("../data/types.mjs").TagsInTaxonomy} TagsInTaxonomy */
/** @typedef {import("../data/types.mjs").StagedTagData} StagedTagData */
/* istanbul ignore next */
export const ContentTagsDrawerContext = React.createContext({
stagedContentTags: /** @type{Record<number, StagedTagData[]>} */ ({}),
globalStagedContentTags: /** @type{Record<number, StagedTagData[]>} */ ({}),
globalStagedRemovedContentTags: /** @type{Record<number, string>} */ ({}),
addStagedContentTag: /** @type{(taxonomyId: number, addedTag: StagedTagData) => void} */ (() => {}),
removeStagedContentTag: /** @type{(taxonomyId: number, tagValue: string) => void} */ (() => {}),
removeGlobalStagedContentTag: /** @type{(taxonomyId: number, tagValue: string) => void} */ (() => {}),
addRemovedContentTag: /** @type{(taxonomyId: number, tagValue: string) => void} */ (() => {}),
deleteRemovedContentTag: /** @type{(taxonomyId: number, tagValue: string) => void} */ (() => {}),
setStagedTags: /** @type{(taxonomyId: number, tagsList: StagedTagData[]) => void} */ (() => {}),
setGlobalStagedContentTags: /** @type{Function} */ (() => {}),
commitGlobalStagedTags: /** @type{() => void} */ (() => {}),
commitGlobalStagedTagsStatus: /** @type{null|string} */ (null),
isContentDataLoaded: /** @type{boolean} */ (false),
isContentTaxonomyTagsLoaded: /** @type{boolean} */ (false),
isTaxonomyListLoaded: /** @type{boolean} */ (false),
contentName: /** @type{string} */ (''),
tagsByTaxonomy: /** @type{TagsInTaxonomy[]} */ ([]),
isEditMode: /** @type{boolean} */ (false),
toEditMode: /** @type{() => void} */ (() => {}),
toReadMode: /** @type{() => void} */ (() => {}),
collapsibleStates: /** @type{Record<number, boolean>} */ ({}),
openCollapsible: /** @type{(taxonomyId: number) => void} */ (() => {}),
closeCollapsible: /** @type{(taxonomyId: number) => void} */ (() => {}),
toastMessage: /** @type{string|undefined} */ (undefined),
showToastAfterSave: /** @type{() => void} */ (() => {}),
closeToast: /** @type{() => void} */ (() => {}),
setCollapsibleToInitalState: /** @type{() => void} */ (() => {}),
otherTaxonomies: /** @type{TagsInTaxonomy[]} */ ([]),
});
// This context has not been added to ContentTagsDrawerContext because it has been
// created one level higher to control the behavior of the Sheet that contatins the Drawer.
// This logic is not used in legacy edx-platform screens. But it can be separated if we keep
// the contexts separate.
// TODO We can join both contexts when the Drawer is no longer used on edx-platform
/* istanbul ignore next */
export const ContentTagsDrawerSheetContext = React.createContext({
blockingSheet: /** @type{boolean} */ (false),
setBlockingSheet: /** @type{Function} */ (() => {}),
});

View File

@@ -0,0 +1,77 @@
import React from 'react';
import type { TagsInTaxonomy, StagedTagData } from '../data/types';
export interface ContentTagsDrawerContextData {
stagedContentTags: Record<number, StagedTagData[]>;
globalStagedContentTags: Record<number, StagedTagData[]>;
globalStagedRemovedContentTags: Record<number, string>;
addStagedContentTag: (taxonomyId: number, addedTag: StagedTagData) => void;
removeStagedContentTag: (taxonomyId: number, tagValue: string) => void;
removeGlobalStagedContentTag: (taxonomyId: number, tagValue: string) => void;
addRemovedContentTag: (taxonomyId: number, tagValue: string) => void;
deleteRemovedContentTag: (taxonomyId: number, tagValue: string) => void;
setStagedTags: (taxonomyId: number, tagsList: StagedTagData[]) => void;
setGlobalStagedContentTags: Function;
commitGlobalStagedTags: () => void;
commitGlobalStagedTagsStatus: null | string;
isContentDataLoaded: boolean;
isContentTaxonomyTagsLoaded: boolean;
isTaxonomyListLoaded: boolean;
contentName: string;
tagsByTaxonomy: TagsInTaxonomy[];
isEditMode: boolean;
toEditMode: () => void;
toReadMode: () => void;
collapsibleStates: Record<number, boolean>;
openCollapsible: (taxonomyId: number) => void;
closeCollapsible: (taxonomyId: number) => void;
toastMessage: string | undefined;
showToastAfterSave: () => void;
closeToast: () => void;
setCollapsibleToInitalState: () => void;
otherTaxonomies: TagsInTaxonomy[];
}
/* istanbul ignore next */
export const ContentTagsDrawerContext = React.createContext<ContentTagsDrawerContextData>({
stagedContentTags: {},
globalStagedContentTags: {},
globalStagedRemovedContentTags: {},
addStagedContentTag: () => {},
removeStagedContentTag: () => {},
removeGlobalStagedContentTag: () => {},
addRemovedContentTag: () => {},
deleteRemovedContentTag: () => {},
setStagedTags: () => {},
setGlobalStagedContentTags: () => {},
commitGlobalStagedTags: () => {},
commitGlobalStagedTagsStatus: null,
isContentDataLoaded: false,
isContentTaxonomyTagsLoaded: false,
isTaxonomyListLoaded: false,
contentName: '',
tagsByTaxonomy: [],
isEditMode: false,
toEditMode: () => {},
toReadMode: () => {},
collapsibleStates: {},
openCollapsible: () => {},
closeCollapsible: () => {},
toastMessage: undefined,
showToastAfterSave: () => {},
closeToast: () => {},
setCollapsibleToInitalState: () => {},
otherTaxonomies: [],
});
// This context has not been added to ContentTagsDrawerContext because it has been
// created one level higher to control the behavior of the Sheet that contatins the Drawer.
// This logic is not used in legacy edx-platform screens. But it can be separated if we keep
// the contexts separate.
// TODO We can join both contexts when the Drawer is no longer used on edx-platform
/* istanbul ignore next */
export const ContentTagsDrawerSheetContext = React.createContext({
blockingSheet: false,
setBlockingSheet: (() => {}) as (blockingSheet: boolean) => void,
});

View File

@@ -38,7 +38,7 @@ export const getContentTaxonomyTagsCountApiUrl = (contentId) => new URL(`api/con
* Get all tags that belong to taxonomy.
* @param {number} taxonomyId The id of the taxonomy to fetch tags for
* @param {{page?: number, searchTerm?: string, parentTag?: string}} options
* @returns {Promise<import("../../taxonomy/tag-list/data/types.mjs").TagListData>}
* @returns {Promise<import("../../taxonomy/data/types.js").TagListData>}
*/
export async function getTaxonomyTagsData(taxonomyId, options = {}) {
const url = getTaxonomyTagsApiUrl(taxonomyId, options);
@@ -49,7 +49,7 @@ export async function getTaxonomyTagsData(taxonomyId, options = {}) {
/**
* Get the tags that are applied to the content object
* @param {string} contentId The id of the content object to fetch the applied tags for
* @returns {Promise<import("./types.mjs").ContentTaxonomyTagsData>}
* @returns {Promise<import("./types.js").ContentTaxonomyTagsData>}
*/
export async function getContentTaxonomyTagsData(contentId) {
const { data } = await getAuthenticatedHttpClient().get(getContentTaxonomyTagsApiUrl(contentId));
@@ -70,12 +70,13 @@ export async function getContentTaxonomyTagsCount(contentId) {
}
/**
* Fetch meta data (eg: display_name) about the content object (unit/compoenent)
* Fetch meta data (eg: display_name) about the content object (unit/component)
* @param {string} contentId The id of the content object (unit/component)
* @returns {Promise<import("./types.mjs").ContentData>}
* @returns {Promise<import("./types.js").ContentData>}
*/
export async function getContentData(contentId) {
let url;
if (contentId.startsWith('lb:')) {
url = getLibraryContentDataApiUrl(contentId);
} else if (contentId.startsWith('course-v1:')) {
@@ -90,8 +91,8 @@ export async function getContentData(contentId) {
/**
* Update content object's applied tags
* @param {string} contentId The id of the content object (unit/component)
* @param {Promise<import("./types.mjs").UpdateTagsData[]>} tagsData The list of tags (values) to set on content object
* @returns {Promise<import("./types.mjs").ContentTaxonomyTagsData>}
* @param {Promise<import("./types.js").UpdateTagsData[]>} tagsData The list of tags (values) to set on content object
* @returns {Promise<import("./types.js").ContentTaxonomyTagsData>}
*/
export async function updateContentTaxonomyTags(contentId, tagsData) {
const url = getContentTaxonomyTagsApiUrl(contentId);

View File

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

View File

@@ -7,6 +7,7 @@ import {
useMutation,
useQueryClient,
} from '@tanstack/react-query';
import { useParams } from 'react-router';
import {
getTaxonomyTagsData,
getContentTaxonomyTagsData,
@@ -14,9 +15,11 @@ import {
updateContentTaxonomyTags,
getContentTaxonomyTagsCount,
} from './api';
import { libraryAuthoringQueryKeys, libraryQueryPredicate, xblockQueryKeys } from '../../library-authoring/data/apiHooks';
import { getLibraryId } from '../../generic/key-utils';
/** @typedef {import("../../taxonomy/tag-list/data/types.mjs").TagListData} TagListData */
/** @typedef {import("../../taxonomy/tag-list/data/types.mjs").TagData} TagData */
/** @typedef {import("../../taxonomy/data/types.js").TagListData} TagListData */
/** @typedef {import("../../taxonomy/data/types.js").TagData} TagData */
/**
* Builds the query to get the taxonomy tags
@@ -110,11 +113,13 @@ export const useContentTaxonomyTagsData = (contentId) => (
/**
* Builds the query to get meta data about the content object
* @param {string} contentId The id of the content object (unit/component)
* @param {boolean} enabled Flag to enable/disable the query
*/
export const useContentData = (contentId) => (
export const useContentData = (contentId, enabled) => (
useQuery({
queryKey: ['contentData', contentId],
queryFn: () => getContentData(contentId),
queryFn: enabled ? () => getContentData(contentId) : undefined,
enabled,
})
);
@@ -124,6 +129,8 @@ export const useContentData = (contentId) => (
*/
export const useContentTaxonomyTagsUpdater = (contentId) => {
const queryClient = useQueryClient();
const unitIframe = window.frames['xblock-iframe'];
const { unitId } = useParams();
return useMutation({
/**
@@ -131,7 +138,7 @@ export const useContentTaxonomyTagsUpdater = (contentId) => {
* any,
* any,
* {
* tagsData: Promise<import("./types.mjs").UpdateTagsData[]>
* tagsData: Promise<import("./types.js").UpdateTagsData[]>
* }
* >}
*/
@@ -146,11 +153,24 @@ export const useContentTaxonomyTagsUpdater = (contentId) => {
contentPattern = contentId.replace(/\+type@.*$/, '*');
}
queryClient.invalidateQueries({ queryKey: ['contentTagsCount', contentPattern] });
if (contentId.startsWith('lb:') || contentId.startsWith('lib-collection:') || contentId.startsWith('lct:')) {
// Obtain library id from contentId
const libraryId = getLibraryId(contentId);
// Invalidate component metadata to update tags count
queryClient.invalidateQueries(xblockQueryKeys.componentMetadata(contentId));
// Invalidate content search to update tags count
queryClient.invalidateQueries(['content_search'], { predicate: (query) => libraryQueryPredicate(query, libraryId) });
// If the tags for a compoent were edited from Unit page, invalidate children query to fetch count again.
if (unitId) {
queryClient.invalidateQueries(libraryAuthoringQueryKeys.containerChildren(unitId));
}
}
},
onSuccess: /* istanbul ignore next */ () => {
/* istanbul ignore next */
if (window.top != null) {
// This send messages to the parent page if the drawer is called from a iframe.
// Sends messages to the parent page if the drawer was opened
// from an iframe or the unit iframe within the course.
// Is used on Studio to update tags data and counts.
// In the future, when the Course Outline Page and Unit Page are integrated into this MFE,
// they should just use React Query to load the tag counts, and React Query will automatically
@@ -159,26 +179,32 @@ export const useContentTaxonomyTagsUpdater = (contentId) => {
// Sends content tags.
getContentTaxonomyTagsData(contentId).then((data) => {
const contentData = {
contentId,
...data,
const contentData = { contentId, ...data };
const message = {
type: 'authoring.events.tags.updated',
data: contentData,
};
window.top?.postMessage(
{ type: 'authoring.events.tags.updated', data: contentData },
getConfig().STUDIO_BASE_URL,
);
const targetOrigin = getConfig().STUDIO_BASE_URL;
unitIframe?.postMessage(message, targetOrigin);
window.top?.postMessage(message, targetOrigin);
});
// Sends tags count.
getContentTaxonomyTagsCount(contentId).then((data) => {
const contentData = {
contentId,
count: data,
getContentTaxonomyTagsCount(contentId).then((count) => {
const contentData = { contentId, count };
const message = {
type: 'authoring.events.tags.count.updated',
data: contentData,
};
window.top?.postMessage(
{ type: 'authoring.events.tags.count.updated', data: contentData },
getConfig().STUDIO_BASE_URL,
);
const targetOrigin = getConfig().STUDIO_BASE_URL;
unitIframe?.postMessage(message, targetOrigin);
window.top?.postMessage(message, targetOrigin);
});
}
},

View File

@@ -1,6 +1,5 @@
import { useQuery, useMutation, useQueries } from '@tanstack/react-query';
import { act } from '@testing-library/react';
import { renderHook } from '@testing-library/react-hooks';
import { act, renderHook } from '@testing-library/react';
import {
useTaxonomyTagsData,
useContentTaxonomyTagsData,
@@ -158,7 +157,7 @@ describe('useContentTaxonomyTagsUpdater', () => {
const contentId = 'testerContent';
const taxonomyId = 123;
const mutation = useContentTaxonomyTagsUpdater(contentId);
const mutation = renderHook(() => useContentTaxonomyTagsUpdater(contentId)).result.current;
const tagsData = [{
taxonomy: taxonomyId,
tags: ['tag1', 'tag2'],

View File

@@ -1,101 +0,0 @@
// @ts-check
/**
* @typedef {Object} Tag A tag that has been applied to some content.
* @property {string} value The value of the tag, also its ID. e.g. "Biology"
* @property {string[]} lineage The values of the tag and its parent(s) in the hierarchy
* @property {boolean} canChangeObjecttag
* @property {boolean} canDeleteObjecttag
*/
/**
* @typedef {Object} ContentTaxonomyTagData A list of the tags from one taxonomy that are applied to a content object.
* @property {string} name
* @property {number} taxonomyId
* @property {boolean} canTagObject
* @property {Tag[]} tags
* @property {string} exportId
*/
/**
* @typedef {Object} ContentTaxonomyTagsData A list of all the tags applied to some content object, grouped by taxonomy.
* @property {ContentTaxonomyTagData[]} taxonomies
*/
/**
* @typedef {Object} ContentActions
* @property {boolean} deleteable
* @property {boolean} draggable
* @property {boolean} childAddable
* @property {boolean} duplicable
*/
/**
* @typedef {Object} XBlockData
* @property {string} id
* @property {string} displayName
* @property {string} category
* @property {boolean} hasChildren
* @property {string} editedOn
* @property {boolean} published
* @property {string} publishedOn
* @property {string} studioUrl
* @property {boolean} releasedToStudents
* @property {string|null} releaseDate
* @property {string} visibilityState
* @property {boolean} hasExplicitStaffLock
* @property {string} start
* @property {boolean} graded
* @property {string} dueDate
* @property {string} due
* @property {string|null} relativeWeeksDue
* @property {string|null} format
* @property {boolean} hasChanges
* @property {ContentActions} actions
* @property {string} explanatoryMessage
* @property {string} showCorrectness
* @property {boolean} discussionEnabled
* @property {boolean} ancestorHasStaffLock
* @property {boolean} staffOnlyMessage
* @property {boolean} hasPartitionGroupComponents
*/
/**
* @typedef {Object} TagsInTaxonomy
* @property {boolean} allOrgs
* @property {boolean} allowFreeText
* @property {boolean} allowMultiple
* @property {boolean} canChangeTaxonomy
* @property {boolean} canDeleteTaxonomy
* @property {boolean} canTagObject
* @property {Tag[]} contentTags
* @property {string} description
* @property {boolean} enabled
* @property {string} exportId
* @property {number} id
* @property {string} name
* @property {boolean} systemDefined
* @property {number} tagsCount
* @property {boolean} visibleToAuthors
*/
/**
* @typedef {Object} CourseData
* @property {string} courseDisplayNameWithDefault
*/
/**
* @typedef {XBlockData | CourseData} ContentData
*/
/**
* @typedef {Object} UpdateTagsData
* @property {number} taxonomy
* @property {string[]} tags
*/
/**
* @typedef {Object} StagedTagData
* @property {string} value
* @property {string} label
*/

View File

@@ -0,0 +1,81 @@
import type { TaxonomyData } from '../../taxonomy/data/types';
/** A tag that has been applied to some content. */
export interface Tag {
/** The value of the tag, also its ID. e.g. "Biology" */
value: string;
/** The values of the tag and its parent(s) in the hierarchy */
lineage: string[];
canChangeObjecttag: boolean;
canDeleteObjecttag: boolean;
}
/** A list of the tags from one taxonomy that are applied to a content object. */
export interface ContentTaxonomyTagData {
name: string;
taxonomyId: number;
canTagObject: boolean;
tags: Tag[];
exportId: string;
}
/** A list of all the tags applied to some content object, grouped by taxonomy. */
export interface ContentTaxonomyTagsData {
taxonomies: ContentTaxonomyTagData[];
}
export interface ContentActions {
deleteable: boolean;
draggable: boolean;
childAddable: boolean;
duplicable: boolean;
}
export interface XBlockData {
id: string;
displayName: string;
category: string;
hasChildren: boolean;
editedOn: string;
published: boolean;
publishedOn: string;
studioUrl: string;
releasedToStudents: boolean;
releaseDate: string | null;
visibilityState: string;
hasExplicitStaffLock: boolean;
start: string;
graded: boolean;
dueDate: string;
due: string;
relativeWeeksDue: string | null;
format: string | null;
hasChanges: boolean;
actions: ContentActions;
explanatoryMessage: string;
showCorrectness: string;
discussionEnabled: boolean;
ancestorHasStaffLock: boolean;
staffOnlyMessage: boolean;
hasPartitionGroupComponents: boolean;
}
export interface TagsInTaxonomy extends TaxonomyData {
contentTags: Tag[];
}
export interface CourseData {
courseDisplayNameWithDefault: string;
}
export type ContentData = XBlockData | CourseData;
export interface UpdateTagsData {
taxonomy: number;
tags: string[];
}
export interface StagedTagData {
value: string;
label: string;
}

View File

@@ -1,4 +1,3 @@
// eslint-disable-next-line import/prefer-default-export
export { default as ContentTagsDrawer } from './ContentTagsDrawer';
// eslint-disable-next-line import/prefer-default-export
export { default as ContentTagsDrawerSheet } from './ContentTagsDrawerSheet';
export { useContentTaxonomyTagsData } from './data/apiHooks';

View File

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

View File

@@ -1,5 +1,4 @@
// @ts-check
import React, { useState, useMemo } from 'react';
import { useState, useMemo } from 'react';
import {
Card, Stack, Button, Collapsible, Icon,
} from '@openedx/paragon';
@@ -10,10 +9,19 @@ import { ContentTagsDrawerSheet } from '..';
import messages from '../messages';
import { useContentTaxonomyTagsData } from '../data/apiHooks';
import type { ContentTaxonomyTagData, Tag } from '../data/types';
import { LoadingSpinner } from '../../generic/Loading';
import TagsTree from '../TagsTree';
const TagsSidebarBody = () => {
interface TagsSidebarBodyProps {
readOnly: boolean
}
type TagTree = {
[key: string]: { children: TagTree, canChangeObjecttag: boolean, canDeleteObjecttag: boolean }
};
const TagsSidebarBody = ({ readOnly }: TagsSidebarBodyProps) => {
const intl = useIntl();
const [showManageTags, setShowManageTags] = useState(false);
const contentId = useParams().blockId;
@@ -24,8 +32,8 @@ const TagsSidebarBody = () => {
isSuccess: isContentTaxonomyTagsLoaded,
} = useContentTaxonomyTagsData(contentId || '');
const buildTagsTree = (contentTags) => {
const resultTree = {};
const buildTagsTree = (contentTags: Tag[]) => {
const resultTree: TagTree = {};
contentTags.forEach(item => {
let currentLevel = resultTree;
@@ -46,7 +54,7 @@ const TagsSidebarBody = () => {
};
const tree = useMemo(() => {
const result = [];
const result: (Omit<ContentTaxonomyTagData, 'tags'> & { tags: TagTree })[] = [];
if (isContentTaxonomyTagsLoaded && contentTaxonomyTagsData) {
contentTaxonomyTagsData.taxonomies.forEach((taxonomy) => {
result.push({
@@ -88,7 +96,13 @@ const TagsSidebarBody = () => {
</div>
)}
<Button className="mt-3 ml-2" variant="outline-primary" size="sm" onClick={() => setShowManageTags(true)}>
<Button
className="mt-3 ml-2"
variant="outline-primary"
size="sm"
onClick={() => setShowManageTags(true)}
disabled={readOnly}
>
{intl.formatMessage(messages.manageTagsButton)}
</Button>
</Stack>
@@ -102,6 +116,4 @@ const TagsSidebarBody = () => {
);
};
TagsSidebarBody.propTypes = {};
export default TagsSidebarBody;

View File

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

View File

@@ -1,10 +1,14 @@
import TagsSidebarHeader from './TagsSidebarHeader';
import TagsSidebarBody from './TagsSidebarBody';
const TagsSidebarControls = () => (
interface TagsSidebarControlsProps {
readOnly: boolean,
}
const TagsSidebarControls = ({ readOnly }: TagsSidebarControlsProps) => (
<>
<TagsSidebarHeader />
<TagsSidebarBody />
<TagsSidebarBody readOnly={readOnly} />
</>
);

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