diff --git a/src/constants.js b/src/constants.js index 2b2d394c0d..eb1b17b372 100644 --- a/src/constants.js +++ b/src/constants.js @@ -20,6 +20,7 @@ export const BADGE_STATES = { }; export const NOTIFICATION_MESSAGES = { + adding: 'Adding', saving: 'Saving', duplicating: 'Duplicating', deleting: 'Deleting', diff --git a/src/course-unit/CourseUnit.jsx b/src/course-unit/CourseUnit.jsx index e7a09a2d02..b532fe0be2 100644 --- a/src/course-unit/CourseUnit.jsx +++ b/src/course-unit/CourseUnit.jsx @@ -12,6 +12,7 @@ import getPageHeadTitle from '../generic/utils'; import ProcessingNotification from '../generic/processing-notification'; import InternetConnectionAlert from '../generic/internet-connection-alert'; import Loading from '../generic/Loading'; +import AddComponent from './add-component/AddComponent'; import HeaderTitle from './header-title/HeaderTitle'; import Breadcrumbs from './breadcrumbs/Breadcrumbs'; import HeaderNavigations from './header-navigations/HeaderNavigations'; @@ -33,6 +34,7 @@ const CourseUnit = ({ courseId }) => { headerNavigationsActions, handleTitleEdit, handleInternetConnectionFailed, + handleCreateNewCourseXblock, } = useCourseUnit({ courseId, blockId }); document.title = getPageHeadTitle('', unitTitle); @@ -87,9 +89,12 @@ const CourseUnit = ({ courseId }) => { xl={[{ span: 9 }, { span: 3 }]} > - {/* TODO: Unit content will be added in the following tasks. */} - Unit content + + diff --git a/src/course-unit/CourseUnit.scss b/src/course-unit/CourseUnit.scss index 954bbbdacc..d3264d89f2 100644 --- a/src/course-unit/CourseUnit.scss +++ b/src/course-unit/CourseUnit.scss @@ -1,2 +1,3 @@ @import "./breadcrumbs/Breadcrumbs"; @import "./course-sequence/CourseSequence"; +@import "./add-component/AddComponent"; diff --git a/src/course-unit/CourseUnit.test.jsx b/src/course-unit/CourseUnit.test.jsx index bc4e1ad9a6..2019dfe6f3 100644 --- a/src/course-unit/CourseUnit.test.jsx +++ b/src/course-unit/CourseUnit.test.jsx @@ -9,14 +9,19 @@ import { getConfig, initializeMockApp } from '@edx/frontend-platform'; import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth'; import { + getCourseSectionVerticalApiUrl, getCourseUnitApiUrl, getXBlockBaseApiUrl, + postXBlockBaseApiUrl, } from './data/api'; import { + fetchCourseSectionVerticalData, fetchCourseUnitQuery, } from './data/thunk'; import initializeStore from '../store'; import { + courseCreateXblockMock, + courseSectionVerticalMock, courseUnitIndexMock, } from './__mocks__'; import { executeThunk } from '../utils'; @@ -24,6 +29,7 @@ import CourseUnit from './CourseUnit'; import headerNavigationsMessages from './header-navigations/messages'; import headerTitleMessages from './header-title/messages'; import { getUnitPreviewPath, getUnitViewLivePath } from './utils'; +import messages from './add-component/messages'; let axiosMock; let store; @@ -32,10 +38,12 @@ const sectionId = 'graded_interactions'; const subsectionId = '19a30717eff543078a5d94ae9d6c18a5'; const blockId = '567890'; const unitDisplayName = courseUnitIndexMock.metadata.display_name; +const mockedUsedNavigate = jest.fn(); jest.mock('react-router-dom', () => ({ ...jest.requireActual('react-router-dom'), useParams: () => ({ blockId }), + useNavigate: () => mockedUsedNavigate, })); const RootWrapper = () => ( @@ -63,6 +71,10 @@ describe('', () => { .onGet(getCourseUnitApiUrl(courseId)) .reply(200, courseUnitIndexMock); await executeThunk(fetchCourseUnitQuery(courseId), store.dispatch); + axiosMock + .onGet(getCourseSectionVerticalApiUrl(blockId)) + .reply(200, courseSectionVerticalMock); + await executeThunk(fetchCourseSectionVerticalData(blockId), store.dispatch); }); it('render CourseUnit component correctly', async () => { @@ -146,4 +158,57 @@ describe('', () => { expect(titleEditField).not.toBeInTheDocument(); expect(await findByText(newDisplayName)).toBeInTheDocument(); }); + + it('doesn\'t handle creating xblock and displays an error message', async () => { + const { courseKey, locator } = courseCreateXblockMock; + axiosMock + .onPost(postXBlockBaseApiUrl({ type: 'video', category: 'video', parentLocator: blockId })) + .reply(500, {}); + const { getByRole } = render(); + + await waitFor(() => { + const videoButton = getByRole('button', { + name: new RegExp(`${messages.buttonText.defaultMessage} Video`, 'i'), + }); + + userEvent.click(videoButton); + expect(mockedUsedNavigate).not.toHaveBeenCalledWith(`/course/${courseKey}/editor/video/${locator}`); + }); + }); + + it('handle creating Problem xblock and navigate to editor page', async () => { + const { courseKey, locator } = courseCreateXblockMock; + axiosMock + .onPost(postXBlockBaseApiUrl({ type: 'problem', category: 'problem', parentLocator: blockId })) + .reply(200, courseCreateXblockMock); + const { getByRole } = render(); + + await waitFor(() => { + const problemButton = getByRole('button', { + name: new RegExp(`${messages.buttonText.defaultMessage} Problem`, 'i'), + }); + + userEvent.click(problemButton); + expect(mockedUsedNavigate).toHaveBeenCalled(); + expect(mockedUsedNavigate).toHaveBeenCalledWith(`/course/${courseKey}/editor/problem/${locator}`); + }); + }); + + it('handles creating Video xblock and navigates to editor page', async () => { + const { courseKey, locator } = courseCreateXblockMock; + axiosMock + .onPost(postXBlockBaseApiUrl({ type: 'video', category: 'video', parentLocator: blockId })) + .reply(200, courseCreateXblockMock); + const { getByRole } = render(); + + await waitFor(() => { + const videoButton = getByRole('button', { + name: new RegExp(`${messages.buttonText.defaultMessage} Video`, 'i'), + }); + + userEvent.click(videoButton); + expect(mockedUsedNavigate).toHaveBeenCalled(); + expect(mockedUsedNavigate).toHaveBeenCalledWith(`/course/${courseKey}/editor/video/${locator}`); + }); + }); }); diff --git a/src/course-unit/__mocks__/courseCreateXblock.js b/src/course-unit/__mocks__/courseCreateXblock.js new file mode 100644 index 0000000000..7da6d4906f --- /dev/null +++ b/src/course-unit/__mocks__/courseCreateXblock.js @@ -0,0 +1,4 @@ +module.exports = { + locator: 'block-v1:edX+L153+3T2023+type@drag-and-drop-v2+block@dc52e3cf8e6145e39ba5c1ff4888db4b', + courseKey: 'course-v1:edX+L153+3T2023', +}; diff --git a/src/course-unit/__mocks__/courseSectionVertical.js b/src/course-unit/__mocks__/courseSectionVertical.js new file mode 100644 index 0000000000..fdae7cdd56 --- /dev/null +++ b/src/course-unit/__mocks__/courseSectionVertical.js @@ -0,0 +1,1418 @@ +module.exports = { + language_code: 'en', + action: 'view', + xblock: { + display_name: 'Getting Started', + display_type: 'Unit', + category: 'vertical', + }, + is_unit_page: true, + is_collapsible: false, + position: 1, + prev_url: '%2Fcontainer%2Fblock-v1%3AedX%2BDemoX%2BDemo_Course%2Btype%40vertical%2Bblock%40vertical_0270f6de40fc', + next_url: '%2Fcontainer%2Fblock-v1%3AedX%2BDemoX%2BDemo_Course%2Btype%40vertical%2Bblock%404f6c1b4e316a419ab5b6bf30e6c708e9', + new_unit_category: 'vertical', + outline_url: '/course/course-v1:edX+DemoX+Demo_Course?format=concise', + ancestor_xblocks: [ + { + children: [ + { + url: '/course/course-v1:edX+DemoX+Demo_Course?show=block-v1%3AedX%2BDemoX%2BDemo_Course%2Btype%40chapter%2Bblock%40d8a6192ade314473a78242dfeedfbf5b', + display_name: 'Introduction 2', + }, + { + url: '/course/course-v1:edX+DemoX+Demo_Course?show=block-v1%3AedX%2BDemoX%2BDemo_Course%2Btype%40chapter%2Bblock%40interactive_demonstrations', + display_name: 'Example Week 1: Getting Started', + }, + { + url: '/course/course-v1:edX+DemoX+Demo_Course?show=block-v1%3AedX%2BDemoX%2BDemo_Course%2Btype%40chapter%2Bblock%40graded_interactions', + display_name: 'Example Week 2: Get Interactive', + }, + { + url: '/course/course-v1:edX+DemoX+Demo_Course?show=block-v1%3AedX%2BDemoX%2BDemo_Course%2Btype%40chapter%2Bblock%40social_integration', + display_name: 'Example Week 3: Be Social', + }, + { + url: '/course/course-v1:edX+DemoX+Demo_Course?show=block-v1%3AedX%2BDemoX%2BDemo_Course%2Btype%40chapter%2Bblock%401414ffd5143b4b508f739b563ab468b7', + display_name: 'About Exams and Certificates', + }, + ], + title: 'Example Week 1: Getting Started', + is_last: false, + }, + { + children: [ + { + url: '/course/course-v1:edX+DemoX+Demo_Course?show=block-v1%3AedX%2BDemoX%2BDemo_Course%2Btype%40sequential%2Bblock%4019a30717eff543078a5d94ae9d6c18a5', + display_name: 'Lesson 1 - Getting Started', + }, + { + url: '/course/course-v1:edX+DemoX+Demo_Course?show=block-v1%3AedX%2BDemoX%2BDemo_Course%2Btype%40sequential%2Bblock%40basic_questions', + display_name: 'Homework - Question Styles', + }, + ], + title: 'Lesson 1 - Getting Started', + is_last: true, + }, + ], + component_templates: [ + { + type: 'advanced', + templates: [ + { + display_name: 'Annotation', + category: 'annotatable', + boilerplate_name: null, + hinted: false, + tab: 'common', + support_level: true, + }, + { + display_name: 'Video', + category: 'videoalpha', + boilerplate_name: null, + hinted: false, + tab: 'common', + support_level: true, + }, + ], + display_name: 'Advanced', + support_legend: { + show_legend: false, + allow_unsupported_xblocks: false, + documentation_label: 'Your Platform Name Here Support Levels:', + }, + }, + { + type: 'discussion', + templates: [ + { + display_name: 'Discussion', + category: 'discussion', + boilerplate_name: null, + hinted: false, + tab: 'advanced', + support_level: true, + }, + ], + display_name: 'Discussion', + support_legend: { + show_legend: false, + allow_unsupported_xblocks: false, + documentation_label: 'Your Platform Name Here Support Levels:', + }, + }, + { + type: 'library', + templates: [ + { + display_name: 'Randomized Content Block', + category: 'library_content', + boilerplate_name: null, + hinted: false, + tab: 'common', + support_level: true, + }, + ], + display_name: 'Library Content', + support_legend: { + show_legend: false, + allow_unsupported_xblocks: false, + documentation_label: 'Your Platform Name Here Support Levels:', + }, + }, + { + type: 'html', + templates: [ + { + display_name: 'Text', + category: 'html', + boilerplate_name: null, + hinted: false, + tab: 'advanced', + support_level: true, + }, + { + display_name: 'Raw HTML', + category: 'html', + boilerplate_name: 'raw.yaml', + hinted: false, + tab: 'advanced', + support_level: true, + }, + { + display_name: 'Zooming Image Tool', + category: 'html', + boilerplate_name: 'zooming_image.yaml', + hinted: false, + tab: 'advanced', + support_level: true, + }, + { + display_name: 'IFrame Tool', + category: 'html', + boilerplate_name: 'iframe.yaml', + hinted: false, + tab: 'advanced', + support_level: true, + }, + { + display_name: 'Anonymous User ID', + category: 'html', + boilerplate_name: 'anon_user_id.yaml', + hinted: false, + tab: 'advanced', + support_level: true, + }, + { + display_name: 'Announcement', + category: 'html', + boilerplate_name: 'announcement.yaml', + hinted: false, + tab: 'advanced', + support_level: true, + }, + ], + display_name: 'Text', + support_legend: { + show_legend: false, + allow_unsupported_xblocks: false, + documentation_label: 'Your Platform Name Here Support Levels:', + }, + }, + { + type: 'openassessment', + templates: [ + { + display_name: 'Peer Assessment Only', + category: 'openassessment', + boilerplate_name: 'peer-assessment', + hinted: false, + tab: 'advanced', + support_level: true, + }, + { + display_name: 'Self Assessment Only', + category: 'openassessment', + boilerplate_name: 'self-assessment', + hinted: false, + tab: 'advanced', + support_level: true, + }, + { + display_name: 'Staff Assessment Only', + category: 'openassessment', + boilerplate_name: 'staff-assessment', + hinted: false, + tab: 'advanced', + support_level: true, + }, + { + display_name: 'Self Assessment to Peer Assessment', + category: 'openassessment', + boilerplate_name: 'self-to-peer', + hinted: false, + tab: 'advanced', + support_level: true, + }, + { + display_name: 'Self Assessment to Staff Assessment', + category: 'openassessment', + boilerplate_name: 'self-to-staff', + hinted: false, + tab: 'advanced', + support_level: true, + }, + ], + display_name: 'Open Response', + support_legend: { + show_legend: false, + allow_unsupported_xblocks: false, + documentation_label: 'Your Platform Name Here Support Levels:', + }, + }, + { + type: 'problem', + templates: [ + { + display_name: 'Blank Common Problem', + category: 'problem', + boilerplate_name: 'blank_common.yaml', + hinted: false, + tab: 'common', + support_level: true, + }, + ], + display_name: 'Problem', + support_legend: { + show_legend: false, + allow_unsupported_xblocks: false, + documentation_label: 'Your Platform Name Here Support Levels:', + }, + }, + { + type: 'video', + templates: [ + { + display_name: 'Video', + category: 'video', + boilerplate_name: null, + hinted: false, + tab: 'advanced', + support_level: true, + }, + ], + display_name: 'Video', + support_legend: { + show_legend: false, + allow_unsupported_xblocks: false, + documentation_label: 'Your Platform Name Here Support Levels:', + }, + }, + { + type: 'drag-and-drop-v2', + templates: [ + { + display_name: 'Drag and Drop', + category: 'drag-and-drop-v2', + boilerplate_name: null, + hinted: false, + tab: 'advanced', + support_level: true, + }, + ], + display_name: 'Drag and Drop', + support_legend: { + show_legend: false, + allow_unsupported_xblocks: false, + documentation_label: 'Your Platform Name Here Support Levels:', + }, + }, + ], + xblock_info: { + id: 'block-v1:edX+DemoX+Demo_Course+type@vertical+block@867dddb6f55d410caaa9c1eb9c6743ec', + display_name: 'Getting Started', + category: 'vertical', + has_children: true, + edited_on: 'Jan 04, 2024 at 10:32 UTC', + published: true, + published_on: 'Dec 28, 2023 at 10:00 UTC', + studio_url: '/container/block-v1:edX+DemoX+Demo_Course+type@vertical+block@867dddb6f55d410caaa9c1eb9c6743ec', + released_to_students: true, + release_date: 'Feb 05, 2013 at 05:00 UTC', + visibility_state: 'needs_attention', + has_explicit_staff_lock: false, + start: '2013-02-05T05:00:00Z', + graded: false, + due_date: '', + due: null, + relative_weeks_due: null, + format: null, + course_graders: [ + 'Homework', + 'Exam', + ], + has_changes: true, + actions: { + deletable: true, + draggable: true, + childAddable: true, + duplicable: true, + }, + explanatory_message: null, + group_access: {}, + user_partitions: [ + { + id: 50, + name: 'Enrollment Track Groups', + scheme: 'enrollment_track', + groups: [ + { + id: 2, + name: 'Verified Certificate', + selected: false, + deleted: false, + }, + { + id: 1, + name: 'Audit', + selected: false, + deleted: false, + }, + ], + }, + ], + show_correctness: 'always', + discussion_enabled: true, + ancestor_info: { + ancestors: [ + { + id: 'block-v1:edX+DemoX+Demo_Course+type@sequential+block@19a30717eff543078a5d94ae9d6c18a5', + display_name: 'Lesson 1 - Getting Started', + category: 'sequential', + has_children: true, + edited_on: 'Jan 04, 2024 at 10:32 UTC', + published: true, + published_on: 'Dec 28, 2023 at 10:00 UTC', + studio_url: '/course/course-v1:edX+DemoX+Demo_Course?show=block-v1%3AedX%2BDemoX%2BDemo_Course%2Btype%40sequential%2Bblock%4019a30717eff543078a5d94ae9d6c18a5', + released_to_students: true, + release_date: 'Feb 05, 2013 at 05:00 UTC', + visibility_state: 'needs_attention', + has_explicit_staff_lock: false, + start: '2013-02-05T05:00:00Z', + graded: false, + due_date: '', + due: null, + relative_weeks_due: null, + format: null, + course_graders: [ + 'Homework', + 'Exam', + ], + has_changes: null, + actions: { + deletable: true, + draggable: true, + childAddable: true, + duplicable: true, + }, + explanatory_message: null, + group_access: {}, + user_partitions: [ + { + id: 50, + name: 'Enrollment Track Groups', + scheme: 'enrollment_track', + groups: [ + { + id: 2, + name: 'Verified Certificate', + selected: false, + deleted: false, + }, + { + id: 1, + name: 'Audit', + selected: false, + deleted: false, + }, + ], + }, + ], + show_correctness: 'always', + hide_after_due: false, + is_proctored_exam: false, + was_exam_ever_linked_with_external: false, + online_proctoring_rules: '', + is_practice_exam: false, + is_onboarding_exam: false, + is_time_limited: false, + exam_review_rules: '', + default_time_limit_minutes: null, + proctoring_exam_configuration_link: null, + supports_onboarding: false, + show_review_rules: true, + child_info: { + category: 'vertical', + display_name: 'Unit', + children: [ + { + id: 'block-v1:edX+DemoX+Demo_Course+type@vertical+block@867dddb6f55d410caaa9c1eb9c6743ec', + display_name: 'Getting Started', + category: 'vertical', + has_children: true, + edited_on: 'Jan 04, 2024 at 10:32 UTC', + published: true, + published_on: 'Dec 28, 2023 at 10:00 UTC', + studio_url: '/container/block-v1:edX+DemoX+Demo_Course+type@vertical+block@867dddb6f55d410caaa9c1eb9c6743ec', + released_to_students: true, + release_date: 'Feb 05, 2013 at 05:00 UTC', + visibility_state: 'needs_attention', + has_explicit_staff_lock: false, + start: '2013-02-05T05:00:00Z', + graded: false, + due_date: '', + due: null, + relative_weeks_due: null, + format: null, + course_graders: [ + 'Homework', + 'Exam', + ], + has_changes: true, + actions: { + deletable: true, + draggable: true, + childAddable: true, + duplicable: true, + }, + explanatory_message: null, + group_access: {}, + user_partitions: [ + { + id: 50, + name: 'Enrollment Track Groups', + scheme: 'enrollment_track', + groups: [ + { + id: 2, + name: 'Verified Certificate', + selected: false, + deleted: false, + }, + { + id: 1, + name: 'Audit', + selected: false, + deleted: false, + }, + ], + }, + ], + show_correctness: 'always', + discussion_enabled: true, + ancestor_has_staff_lock: false, + user_partition_info: { + selectable_partitions: [ + { + id: 50, + name: 'Enrollment Track Groups', + scheme: 'enrollment_track', + groups: [ + { + id: 2, + name: 'Verified Certificate', + selected: false, + deleted: false, + }, + { + id: 1, + name: 'Audit', + selected: false, + deleted: false, + }, + ], + }, + ], + selected_partition_index: -1, + selected_groups_label: '', + }, + enable_copy_paste_units: false, + }, + { + id: 'block-v1:edX+DemoX+Demo_Course+type@vertical+block@4f6c1b4e316a419ab5b6bf30e6c708e9', + display_name: 'Working with Videos', + category: 'vertical', + has_children: true, + edited_on: 'Dec 28, 2023 at 10:00 UTC', + published: true, + published_on: 'Dec 28, 2023 at 10:00 UTC', + studio_url: '/container/block-v1:edX+DemoX+Demo_Course+type@vertical+block@4f6c1b4e316a419ab5b6bf30e6c708e9', + released_to_students: true, + release_date: 'Feb 05, 2013 at 05:00 UTC', + visibility_state: 'live', + has_explicit_staff_lock: false, + start: '2013-02-05T05:00:00Z', + graded: false, + due_date: '', + due: null, + relative_weeks_due: null, + format: null, + course_graders: [ + 'Homework', + 'Exam', + ], + has_changes: false, + actions: { + deletable: true, + draggable: true, + childAddable: true, + duplicable: true, + }, + explanatory_message: null, + group_access: {}, + user_partitions: [ + { + id: 50, + name: 'Enrollment Track Groups', + scheme: 'enrollment_track', + groups: [ + { + id: 2, + name: 'Verified Certificate', + selected: false, + deleted: false, + }, + { + id: 1, + name: 'Audit', + selected: false, + deleted: false, + }, + ], + }, + ], + show_correctness: 'always', + discussion_enabled: true, + ancestor_has_staff_lock: false, + user_partition_info: { + selectable_partitions: [ + { + id: 50, + name: 'Enrollment Track Groups', + scheme: 'enrollment_track', + groups: [ + { + id: 2, + name: 'Verified Certificate', + selected: false, + deleted: false, + }, + { + id: 1, + name: 'Audit', + selected: false, + deleted: false, + }, + ], + }, + ], + selected_partition_index: -1, + selected_groups_label: '', + }, + enable_copy_paste_units: false, + }, + { + id: 'block-v1:edX+DemoX+Demo_Course+type@vertical+block@3dc16db8d14842e38324e95d4030b8a0', + display_name: 'Videos on edX', + category: 'vertical', + has_children: true, + edited_on: 'Dec 28, 2023 at 10:00 UTC', + published: true, + published_on: 'Dec 28, 2023 at 10:00 UTC', + studio_url: '/container/block-v1:edX+DemoX+Demo_Course+type@vertical+block@3dc16db8d14842e38324e95d4030b8a0', + released_to_students: true, + release_date: 'Feb 05, 2013 at 05:00 UTC', + visibility_state: 'live', + has_explicit_staff_lock: false, + start: '2013-02-05T05:00:00Z', + graded: false, + due_date: '', + due: null, + relative_weeks_due: null, + format: null, + course_graders: [ + 'Homework', + 'Exam', + ], + has_changes: false, + actions: { + deletable: true, + draggable: true, + childAddable: true, + duplicable: true, + }, + explanatory_message: null, + group_access: {}, + user_partitions: [ + { + id: 50, + name: 'Enrollment Track Groups', + scheme: 'enrollment_track', + groups: [ + { + id: 2, + name: 'Verified Certificate', + selected: false, + deleted: false, + }, + { + id: 1, + name: 'Audit', + selected: false, + deleted: false, + }, + ], + }, + ], + show_correctness: 'always', + discussion_enabled: true, + ancestor_has_staff_lock: false, + user_partition_info: { + selectable_partitions: [ + { + id: 50, + name: 'Enrollment Track Groups', + scheme: 'enrollment_track', + groups: [ + { + id: 2, + name: 'Verified Certificate', + selected: false, + deleted: false, + }, + { + id: 1, + name: 'Audit', + selected: false, + deleted: false, + }, + ], + }, + ], + selected_partition_index: -1, + selected_groups_label: '', + }, + enable_copy_paste_units: false, + }, + { + id: 'block-v1:edX+DemoX+Demo_Course+type@vertical+block@4a1bba2a403f40bca5ec245e945b0d76', + display_name: 'Video Demonstrations', + category: 'vertical', + has_children: true, + edited_on: 'Dec 28, 2023 at 10:00 UTC', + published: true, + published_on: 'Dec 28, 2023 at 10:00 UTC', + studio_url: '/container/block-v1:edX+DemoX+Demo_Course+type@vertical+block@4a1bba2a403f40bca5ec245e945b0d76', + released_to_students: true, + release_date: 'Feb 05, 2013 at 05:00 UTC', + visibility_state: 'live', + has_explicit_staff_lock: false, + start: '2013-02-05T05:00:00Z', + graded: false, + due_date: '', + due: null, + relative_weeks_due: null, + format: null, + course_graders: [ + 'Homework', + 'Exam', + ], + has_changes: false, + actions: { + deletable: true, + draggable: true, + childAddable: true, + duplicable: true, + }, + explanatory_message: null, + group_access: {}, + user_partitions: [ + { + id: 50, + name: 'Enrollment Track Groups', + scheme: 'enrollment_track', + groups: [ + { + id: 2, + name: 'Verified Certificate', + selected: false, + deleted: false, + }, + { + id: 1, + name: 'Audit', + selected: false, + deleted: false, + }, + ], + }, + ], + show_correctness: 'always', + discussion_enabled: true, + ancestor_has_staff_lock: false, + user_partition_info: { + selectable_partitions: [ + { + id: 50, + name: 'Enrollment Track Groups', + scheme: 'enrollment_track', + groups: [ + { + id: 2, + name: 'Verified Certificate', + selected: false, + deleted: false, + }, + { + id: 1, + name: 'Audit', + selected: false, + deleted: false, + }, + ], + }, + ], + selected_partition_index: -1, + selected_groups_label: '', + }, + enable_copy_paste_units: false, + }, + { + id: 'block-v1:edX+DemoX+Demo_Course+type@vertical+block@256f17a44983429fb1a60802203ee4e0', + display_name: 'Video Presentation Styles', + category: 'vertical', + has_children: true, + edited_on: 'Dec 28, 2023 at 10:00 UTC', + published: true, + published_on: 'Dec 28, 2023 at 10:00 UTC', + studio_url: '/container/block-v1:edX+DemoX+Demo_Course+type@vertical+block@256f17a44983429fb1a60802203ee4e0', + released_to_students: true, + release_date: 'Feb 05, 2013 at 05:00 UTC', + visibility_state: 'live', + has_explicit_staff_lock: false, + start: '2013-02-05T05:00:00Z', + graded: false, + due_date: '', + due: null, + relative_weeks_due: null, + format: null, + course_graders: [ + 'Homework', + 'Exam', + ], + has_changes: false, + actions: { + deletable: true, + draggable: true, + childAddable: true, + duplicable: true, + }, + explanatory_message: null, + group_access: {}, + user_partitions: [ + { + id: 50, + name: 'Enrollment Track Groups', + scheme: 'enrollment_track', + groups: [ + { + id: 2, + name: 'Verified Certificate', + selected: false, + deleted: false, + }, + { + id: 1, + name: 'Audit', + selected: false, + deleted: false, + }, + ], + }, + ], + show_correctness: 'always', + discussion_enabled: true, + ancestor_has_staff_lock: false, + user_partition_info: { + selectable_partitions: [ + { + id: 50, + name: 'Enrollment Track Groups', + scheme: 'enrollment_track', + groups: [ + { + id: 2, + name: 'Verified Certificate', + selected: false, + deleted: false, + }, + { + id: 1, + name: 'Audit', + selected: false, + deleted: false, + }, + ], + }, + ], + selected_partition_index: -1, + selected_groups_label: '', + }, + enable_copy_paste_units: false, + }, + { + id: 'block-v1:edX+DemoX+Demo_Course+type@vertical+block@e3601c0abee6427d8c17e6d6f8fdddd1', + display_name: 'Interactive Questions', + category: 'vertical', + has_children: true, + edited_on: 'Dec 28, 2023 at 10:00 UTC', + published: true, + published_on: 'Dec 28, 2023 at 10:00 UTC', + studio_url: '/container/block-v1:edX+DemoX+Demo_Course+type@vertical+block@e3601c0abee6427d8c17e6d6f8fdddd1', + released_to_students: true, + release_date: 'Feb 05, 2013 at 05:00 UTC', + visibility_state: 'live', + has_explicit_staff_lock: false, + start: '2013-02-05T05:00:00Z', + graded: false, + due_date: '', + due: null, + relative_weeks_due: null, + format: null, + course_graders: [ + 'Homework', + 'Exam', + ], + has_changes: false, + actions: { + deletable: true, + draggable: true, + childAddable: true, + duplicable: true, + }, + explanatory_message: null, + group_access: {}, + user_partitions: [ + { + id: 50, + name: 'Enrollment Track Groups', + scheme: 'enrollment_track', + groups: [ + { + id: 2, + name: 'Verified Certificate', + selected: false, + deleted: false, + }, + { + id: 1, + name: 'Audit', + selected: false, + deleted: false, + }, + ], + }, + ], + show_correctness: 'always', + discussion_enabled: true, + ancestor_has_staff_lock: false, + user_partition_info: { + selectable_partitions: [ + { + id: 50, + name: 'Enrollment Track Groups', + scheme: 'enrollment_track', + groups: [ + { + id: 2, + name: 'Verified Certificate', + selected: false, + deleted: false, + }, + { + id: 1, + name: 'Audit', + selected: false, + deleted: false, + }, + ], + }, + ], + selected_partition_index: -1, + selected_groups_label: '', + }, + enable_copy_paste_units: false, + }, + { + id: 'block-v1:edX+DemoX+Demo_Course+type@vertical+block@a79d59cd72034188a71d388f4954a606', + display_name: 'Exciting Labs and Tools', + category: 'vertical', + has_children: true, + edited_on: 'Dec 28, 2023 at 10:00 UTC', + published: true, + published_on: 'Dec 28, 2023 at 10:00 UTC', + studio_url: '/container/block-v1:edX+DemoX+Demo_Course+type@vertical+block@a79d59cd72034188a71d388f4954a606', + released_to_students: true, + release_date: 'Feb 05, 2013 at 05:00 UTC', + visibility_state: 'live', + has_explicit_staff_lock: false, + start: '2013-02-05T05:00:00Z', + graded: false, + due_date: '', + due: null, + relative_weeks_due: null, + format: null, + course_graders: [ + 'Homework', + 'Exam', + ], + has_changes: false, + actions: { + deletable: true, + draggable: true, + childAddable: true, + duplicable: true, + }, + explanatory_message: null, + group_access: {}, + user_partitions: [ + { + id: 50, + name: 'Enrollment Track Groups', + scheme: 'enrollment_track', + groups: [ + { + id: 2, + name: 'Verified Certificate', + selected: false, + deleted: false, + }, + { + id: 1, + name: 'Audit', + selected: false, + deleted: false, + }, + ], + }, + ], + show_correctness: 'always', + discussion_enabled: true, + ancestor_has_staff_lock: false, + user_partition_info: { + selectable_partitions: [ + { + id: 50, + name: 'Enrollment Track Groups', + scheme: 'enrollment_track', + groups: [ + { + id: 2, + name: 'Verified Certificate', + selected: false, + deleted: false, + }, + { + id: 1, + name: 'Audit', + selected: false, + deleted: false, + }, + ], + }, + ], + selected_partition_index: -1, + selected_groups_label: '', + }, + enable_copy_paste_units: false, + }, + { + id: 'block-v1:edX+DemoX+Demo_Course+type@vertical+block@134df56c516a4a0dbb24dd5facef746e', + display_name: 'Reading Assignments', + category: 'vertical', + has_children: true, + edited_on: 'Dec 28, 2023 at 10:00 UTC', + published: true, + published_on: 'Dec 28, 2023 at 10:00 UTC', + studio_url: '/container/block-v1:edX+DemoX+Demo_Course+type@vertical+block@134df56c516a4a0dbb24dd5facef746e', + released_to_students: true, + release_date: 'Feb 05, 2013 at 05:00 UTC', + visibility_state: 'live', + has_explicit_staff_lock: false, + start: '2013-02-05T05:00:00Z', + graded: false, + due_date: '', + due: null, + relative_weeks_due: null, + format: null, + course_graders: [ + 'Homework', + 'Exam', + ], + has_changes: false, + actions: { + deletable: true, + draggable: true, + childAddable: true, + duplicable: true, + }, + explanatory_message: null, + group_access: {}, + user_partitions: [ + { + id: 50, + name: 'Enrollment Track Groups', + scheme: 'enrollment_track', + groups: [ + { + id: 2, + name: 'Verified Certificate', + selected: false, + deleted: false, + }, + { + id: 1, + name: 'Audit', + selected: false, + deleted: false, + }, + ], + }, + ], + show_correctness: 'always', + discussion_enabled: true, + ancestor_has_staff_lock: false, + user_partition_info: { + selectable_partitions: [ + { + id: 50, + name: 'Enrollment Track Groups', + scheme: 'enrollment_track', + groups: [ + { + id: 2, + name: 'Verified Certificate', + selected: false, + deleted: false, + }, + { + id: 1, + name: 'Audit', + selected: false, + deleted: false, + }, + ], + }, + ], + selected_partition_index: -1, + selected_groups_label: '', + }, + enable_copy_paste_units: false, + }, + { + id: 'block-v1:edX+DemoX+Demo_Course+type@vertical+block@d91b9e5d8bc64d57a1332d06bf2f2193', + display_name: 'When Are Your Exams? ', + category: 'vertical', + has_children: true, + edited_on: 'Dec 28, 2023 at 10:00 UTC', + published: true, + published_on: 'Dec 28, 2023 at 10:00 UTC', + studio_url: '/container/block-v1:edX+DemoX+Demo_Course+type@vertical+block@d91b9e5d8bc64d57a1332d06bf2f2193', + released_to_students: true, + release_date: 'Feb 05, 2013 at 05:00 UTC', + visibility_state: 'live', + has_explicit_staff_lock: false, + start: '2013-02-05T05:00:00Z', + graded: false, + due_date: '', + due: null, + relative_weeks_due: null, + format: null, + course_graders: [ + 'Homework', + 'Exam', + ], + has_changes: false, + actions: { + deletable: true, + draggable: true, + childAddable: true, + duplicable: true, + }, + explanatory_message: null, + group_access: {}, + user_partitions: [ + { + id: 50, + name: 'Enrollment Track Groups', + scheme: 'enrollment_track', + groups: [ + { + id: 2, + name: 'Verified Certificate', + selected: false, + deleted: false, + }, + { + id: 1, + name: 'Audit', + selected: false, + deleted: false, + }, + ], + }, + ], + show_correctness: 'always', + discussion_enabled: true, + ancestor_has_staff_lock: false, + user_partition_info: { + selectable_partitions: [ + { + id: 50, + name: 'Enrollment Track Groups', + scheme: 'enrollment_track', + groups: [ + { + id: 2, + name: 'Verified Certificate', + selected: false, + deleted: false, + }, + { + id: 1, + name: 'Audit', + selected: false, + deleted: false, + }, + ], + }, + ], + selected_partition_index: -1, + selected_groups_label: '', + }, + enable_copy_paste_units: false, + }, + ], + }, + ancestor_has_staff_lock: false, + user_partition_info: { + selectable_partitions: [ + { + id: 50, + name: 'Enrollment Track Groups', + scheme: 'enrollment_track', + groups: [ + { + id: 2, + name: 'Verified Certificate', + selected: false, + deleted: false, + }, + { + id: 1, + name: 'Audit', + selected: false, + deleted: false, + }, + ], + }, + ], + selected_partition_index: -1, + selected_groups_label: '', + }, + }, + { + id: 'block-v1:edX+DemoX+Demo_Course+type@chapter+block@interactive_demonstrations', + display_name: 'Example Week 1: Getting Started', + category: 'chapter', + has_children: true, + edited_on: 'Jan 04, 2024 at 10:32 UTC', + published: true, + published_on: 'Dec 28, 2023 at 10:00 UTC', + studio_url: '/course/course-v1:edX+DemoX+Demo_Course?show=block-v1%3AedX%2BDemoX%2BDemo_Course%2Btype%40chapter%2Bblock%40interactive_demonstrations', + released_to_students: true, + release_date: 'Feb 05, 2013 at 05:00 UTC', + visibility_state: 'live', + has_explicit_staff_lock: false, + start: '2013-02-05T05:00:00Z', + graded: false, + due_date: '', + due: null, + relative_weeks_due: null, + format: null, + course_graders: [ + 'Homework', + 'Exam', + ], + has_changes: null, + actions: { + deletable: true, + draggable: true, + childAddable: true, + duplicable: true, + }, + explanatory_message: null, + group_access: {}, + user_partitions: [ + { + id: 50, + name: 'Enrollment Track Groups', + scheme: 'enrollment_track', + groups: [ + { + id: 2, + name: 'Verified Certificate', + selected: false, + deleted: false, + }, + { + id: 1, + name: 'Audit', + selected: false, + deleted: false, + }, + ], + }, + ], + show_correctness: 'always', + highlights: [], + highlights_enabled: true, + highlights_preview_only: false, + highlights_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/latest/developing_course/course_sections.html#set-section-highlights-for-weekly-course-highlight-messages', + ancestor_has_staff_lock: false, + user_partition_info: { + selectable_partitions: [ + { + id: 50, + name: 'Enrollment Track Groups', + scheme: 'enrollment_track', + groups: [ + { + id: 2, + name: 'Verified Certificate', + selected: false, + deleted: false, + }, + { + id: 1, + name: 'Audit', + selected: false, + deleted: false, + }, + ], + }, + ], + selected_partition_index: -1, + selected_groups_label: '', + }, + }, + { + id: 'block-v1:edX+DemoX+Demo_Course+type@course+block@course', + display_name: 'Demonstration Course', + category: 'course', + has_children: true, + unit_level_discussions: false, + edited_on: 'Jan 08, 2024 at 16:39 UTC', + published: true, + published_on: 'Jan 08, 2024 at 16:39 UTC', + studio_url: '/course/course-v1:edX+DemoX+Demo_Course', + released_to_students: true, + release_date: 'Feb 05, 2013 at 05:00 UTC', + visibility_state: null, + has_explicit_staff_lock: false, + start: '2013-02-05T05:00:00Z', + graded: false, + due_date: '', + due: null, + relative_weeks_due: null, + format: null, + course_graders: [ + 'Homework', + 'Exam', + ], + has_changes: null, + actions: { + deletable: true, + draggable: true, + childAddable: true, + duplicable: true, + }, + explanatory_message: null, + group_access: {}, + user_partitions: [ + { + id: 50, + name: 'Enrollment Track Groups', + scheme: 'enrollment_track', + groups: [ + { + id: 2, + name: 'Verified Certificate', + selected: false, + deleted: false, + }, + { + id: 1, + name: 'Audit', + selected: false, + deleted: false, + }, + ], + }, + ], + show_correctness: 'always', + highlights_enabled_for_messaging: false, + highlights_enabled: true, + highlights_preview_only: false, + highlights_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/latest/developing_course/course_sections.html#set-section-highlights-for-weekly-course-highlight-messages', + enable_proctored_exams: false, + create_zendesk_tickets: true, + enable_timed_exams: true, + ancestor_has_staff_lock: false, + user_partition_info: { + selectable_partitions: [ + { + id: 50, + name: 'Enrollment Track Groups', + scheme: 'enrollment_track', + groups: [ + { + id: 2, + name: 'Verified Certificate', + selected: false, + deleted: false, + }, + { + id: 1, + name: 'Audit', + selected: false, + deleted: false, + }, + ], + }, + ], + selected_partition_index: -1, + selected_groups_label: '', + }, + }, + ], + }, + ancestor_has_staff_lock: false, + user_partition_info: { + selectable_partitions: [ + { + id: 50, + name: 'Enrollment Track Groups', + scheme: 'enrollment_track', + groups: [ + { + id: 2, + name: 'Verified Certificate', + selected: false, + deleted: false, + }, + { + id: 1, + name: 'Audit', + selected: false, + deleted: false, + }, + ], + }, + ], + selected_partition_index: -1, + selected_groups_label: '', + }, + enable_copy_paste_units: false, + edited_by: 'edx', + published_by: null, + currently_visible_to_students: true, + has_partition_group_components: false, + release_date_from: 'Section "Example Week 1: Getting Started"', + staff_lock_from: null, + }, + draft_preview_link: '//preview.localhost:18000/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@vertical+block@867dddb6f55d410caaa9c1eb9c6743ec', + published_preview_link: '//localhost:18000/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@vertical+block@867dddb6f55d410caaa9c1eb9c6743ec', + show_unit_tags: false, + user_clipboard: { + content: null, + source_usage_key: '', + source_context_title: '', + source_edit_url: '', + }, + is_fullwidth_content: false, + assets_url: '/assets/course-v1:edX+DemoX+Demo_Course/', + unit_block_id: '867dddb6f55d410caaa9c1eb9c6743ec', + subsection_location: 'block-v1:edX+DemoX+Demo_Course+type@sequential+block@19a30717eff543078a5d94ae9d6c18a5', +}; diff --git a/src/course-unit/__mocks__/index.js b/src/course-unit/__mocks__/index.js index ebf5206845..a6f44a81d9 100644 --- a/src/course-unit/__mocks__/index.js +++ b/src/course-unit/__mocks__/index.js @@ -1,2 +1,3 @@ -/* eslint-disable import/prefer-default-export */ export { default as courseUnitIndexMock } from './courseUnitIndex'; +export { default as courseSectionVerticalMock } from './courseSectionVertical'; +export { default as courseCreateXblockMock } from './courseCreateXblock'; diff --git a/src/course-unit/add-component/AddComponent.jsx b/src/course-unit/add-component/AddComponent.jsx new file mode 100644 index 0000000000..dd24e4223f --- /dev/null +++ b/src/course-unit/add-component/AddComponent.jsx @@ -0,0 +1,64 @@ +import PropTypes from 'prop-types'; +import { useSelector } from 'react-redux'; +import { useNavigate } from 'react-router-dom'; +import { useIntl } from '@edx/frontend-platform/i18n'; +import { Button } from '@edx/paragon'; + +import { getCourseSectionVertical } from '../data/selectors'; +import { COMPONENT_ICON_TYPES } from '../constants'; +import ComponentIcon from './ComponentIcon'; +import messages from './messages'; + +const AddComponent = ({ blockId, handleCreateNewCourseXblock }) => { + const navigate = useNavigate(); + const intl = useIntl(); + const { componentTemplates } = useSelector(getCourseSectionVertical); + + const handleCreateNewXblock = (type) => () => { + switch (type) { + case COMPONENT_ICON_TYPES.discussion: + case COMPONENT_ICON_TYPES.dragAndDrop: + handleCreateNewCourseXblock({ type, parentLocator: blockId }); + break; + case COMPONENT_ICON_TYPES.problem: + case COMPONENT_ICON_TYPES.video: + handleCreateNewCourseXblock({ type, parentLocator: blockId }, ({ courseKey, locator }) => { + navigate(`/course/${courseKey}/editor/${type}/${locator}`); + }); + break; + default: + } + }; + + if (!Object.keys(componentTemplates).length) { + return null; + } + + return ( +
+
{intl.formatMessage(messages.title)}
+
    + {Object.keys(componentTemplates).map((component) => ( +
  • + +
  • + ))} +
+
+ ); +}; + +AddComponent.propTypes = { + blockId: PropTypes.string.isRequired, + handleCreateNewCourseXblock: PropTypes.func.isRequired, +}; + +export default AddComponent; diff --git a/src/course-unit/add-component/AddComponent.scss b/src/course-unit/add-component/AddComponent.scss new file mode 100644 index 0000000000..aba0a04e1c --- /dev/null +++ b/src/course-unit/add-component/AddComponent.scss @@ -0,0 +1,12 @@ +.course-unit { + .new-component-type { + gap: .75rem; + } + + .add-component-button { + @include pgn-box-shadow(1, "down"); + + width: 11.63rem; + height: 6.875rem; + } +} diff --git a/src/course-unit/add-component/AddComponent.test.jsx b/src/course-unit/add-component/AddComponent.test.jsx new file mode 100644 index 0000000000..44befb33de --- /dev/null +++ b/src/course-unit/add-component/AddComponent.test.jsx @@ -0,0 +1,165 @@ +import MockAdapter from 'axios-mock-adapter'; +import { render } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; + +import { initializeMockApp } from '@edx/frontend-platform'; +import { AppProvider } from '@edx/frontend-platform/react'; +import { IntlProvider } from '@edx/frontend-platform/i18n'; +import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth'; + +import initializeStore from '../../store'; +import { executeThunk } from '../../utils'; +import { fetchCourseSectionVerticalData } from '../data/thunk'; +import { getCourseSectionVerticalApiUrl } from '../data/api'; +import { courseSectionVerticalMock } from '../__mocks__'; +import AddComponent from './AddComponent'; +import messages from './messages'; + +let store; +let axiosMock; +const blockId = '123'; +const handleCreateNewCourseXblockMock = jest.fn(); + +const renderComponent = (props) => render( + + + + + , +); + +describe('', () => { + beforeEach(async () => { + initializeMockApp({ + authenticatedUser: { + userId: 3, + username: 'abc123', + administrator: true, + roles: [], + }, + }); + + store = initializeStore(); + axiosMock = new MockAdapter(getAuthenticatedHttpClient()); + axiosMock + .onGet(getCourseSectionVerticalApiUrl(blockId)) + .reply(200, courseSectionVerticalMock); + await executeThunk(fetchCourseSectionVerticalData(blockId), store.dispatch); + }); + + it('render AddComponent component correctly', () => { + const { getByRole } = renderComponent(); + const componentTemplates = courseSectionVerticalMock.component_templates; + + expect(getByRole('heading', { name: messages.title.defaultMessage })).toBeInTheDocument(); + Object.keys(componentTemplates).map((component) => ( + expect(getByRole('button', { + name: new RegExp(`${messages.buttonText.defaultMessage} ${componentTemplates[component].display_name}`, 'i'), + })).toBeInTheDocument() + )); + }); + + it('doesn\'t render AddComponent component when there aren\'t componentTemplates', async () => { + axiosMock + .onGet(getCourseSectionVerticalApiUrl(blockId)) + .reply(200, { + ...courseSectionVerticalMock, + component_templates: [], + }); + await executeThunk(fetchCourseSectionVerticalData(blockId), store.dispatch); + + const { queryByRole } = renderComponent(); + + expect(queryByRole('heading', { name: messages.title.defaultMessage })).not.toBeInTheDocument(); + }); + + it('does\'t call handleCreateNewCourseXblock with custom component create button is clicked', async () => { + axiosMock + .onGet(getCourseSectionVerticalApiUrl(blockId)) + .reply(200, { + ...courseSectionVerticalMock, + component_templates: [ + { + type: 'custom', + templates: [], + display_name: 'Custom', + support_legend: {}, + }, + ], + }); + await executeThunk(fetchCourseSectionVerticalData(blockId), store.dispatch); + + const { getByRole } = renderComponent(); + + const customComponentButton = getByRole('button', { + name: new RegExp(`${messages.buttonText.defaultMessage} Custom`, 'i'), + }); + + userEvent.click(customComponentButton); + expect(handleCreateNewCourseXblockMock).not.toHaveBeenCalled(); + }); + + it('calls handleCreateNewCourseXblock with correct parameters when Discussion xblock create button is clicked', () => { + const { getByRole } = renderComponent(); + + const discussionButton = getByRole('button', { + name: new RegExp(`${messages.buttonText.defaultMessage} Discussion`, 'i'), + }); + + userEvent.click(discussionButton); + expect(handleCreateNewCourseXblockMock).toHaveBeenCalled(); + expect(handleCreateNewCourseXblockMock).toHaveBeenCalledWith({ + parentLocator: '123', + type: 'discussion', + }); + }); + + it('calls handleCreateNewCourseXblock with correct parameters when Drag-and-Drop xblock create button is clicked', () => { + const { getByRole } = renderComponent(); + + const discussionButton = getByRole('button', { + name: new RegExp(`${messages.buttonText.defaultMessage} Drag and Drop`, 'i'), + }); + + userEvent.click(discussionButton); + expect(handleCreateNewCourseXblockMock).toHaveBeenCalled(); + expect(handleCreateNewCourseXblockMock).toHaveBeenCalledWith({ + parentLocator: '123', + type: 'drag-and-drop-v2', + }); + }); + + it('calls handleCreateNewCourseXblock with correct parameters when Problem xblock create button is clicked', () => { + const { getByRole } = renderComponent(); + + const discussionButton = getByRole('button', { + name: new RegExp(`${messages.buttonText.defaultMessage} Problem`, 'i'), + }); + + userEvent.click(discussionButton); + expect(handleCreateNewCourseXblockMock).toHaveBeenCalled(); + expect(handleCreateNewCourseXblockMock).toHaveBeenCalledWith({ + parentLocator: '123', + type: 'problem', + }, expect.any(Function)); + }); + + it('calls handleCreateNewCourseXblock with correct parameters when Video xblock create button is clicked', () => { + const { getByRole } = renderComponent(); + + const discussionButton = getByRole('button', { + name: new RegExp(`${messages.buttonText.defaultMessage} Video`, 'i'), + }); + + userEvent.click(discussionButton); + expect(handleCreateNewCourseXblockMock).toHaveBeenCalled(); + expect(handleCreateNewCourseXblockMock).toHaveBeenCalledWith({ + parentLocator: '123', + type: 'video', + }, expect.any(Function)); + }); +}); diff --git a/src/course-unit/add-component/ComponentIcon.jsx b/src/course-unit/add-component/ComponentIcon.jsx new file mode 100644 index 0000000000..a1de9ac116 --- /dev/null +++ b/src/course-unit/add-component/ComponentIcon.jsx @@ -0,0 +1,17 @@ +import PropTypes from 'prop-types'; +import { Icon } from '@edx/paragon'; +import { EditNote as EditNoteIcon } from '@edx/paragon/icons'; + +import { COMPONENT_TYPE_ICON_MAP, COMPONENT_ICON_TYPES } from '../constants'; + +const ComponentIcon = ({ type }) => { + const icon = COMPONENT_TYPE_ICON_MAP[type] || EditNoteIcon; + + return ; +}; + +ComponentIcon.propTypes = { + type: PropTypes.oneOf(Object.values(COMPONENT_ICON_TYPES)).isRequired, +}; + +export default ComponentIcon; diff --git a/src/course-unit/add-component/messages.js b/src/course-unit/add-component/messages.js new file mode 100644 index 0000000000..94e6bf4833 --- /dev/null +++ b/src/course-unit/add-component/messages.js @@ -0,0 +1,14 @@ +import { defineMessages } from '@edx/frontend-platform/i18n'; + +const messages = defineMessages({ + title: { + id: 'course-authoring.course-unit.add.component.title', + defaultMessage: 'Add a new component', + }, + buttonText: { + id: 'course-authoring.course-unit.add.component.button.text', + defaultMessage: 'Add Component:', + }, +}); + +export default messages; diff --git a/src/course-unit/constants.js b/src/course-unit/constants.js index c35d980d1b..7427db238e 100644 --- a/src/course-unit/constants.js +++ b/src/course-unit/constants.js @@ -1,13 +1,31 @@ import { + BackHand as BackHandIcon, BookOpen as BookOpenIcon, Edit as EditIcon, + EditNote as EditNoteIcon, FormatListBulleted as FormatListBulletedIcon, + HelpOutline as HelpOutlineIcon, + LibraryAdd as LibraryIcon, Lock as LockIcon, + QuestionAnswerOutline as QuestionAnswerOutlineIcon, + Science as ScienceIcon, + TextFields as TextFieldsIcon, VideoCamera as VideoCameraIcon, } from '@edx/paragon/icons'; export const UNIT_ICON_TYPES = ['video', 'other', 'vertical', 'problem', 'lock']; +export const COMPONENT_ICON_TYPES = { + advanced: 'advanced', + discussion: 'discussion', + library: 'library', + html: 'html', + openassessment: 'openassessment', + problem: 'problem', + video: 'video', + dragAndDrop: 'drag-and-drop-v2', +}; + export const TYPE_ICONS_MAP = { video: VideoCameraIcon, other: BookOpenIcon, @@ -15,3 +33,14 @@ export const TYPE_ICONS_MAP = { problem: EditIcon, lock: LockIcon, }; + +export const COMPONENT_TYPE_ICON_MAP = { + [COMPONENT_ICON_TYPES.advanced]: ScienceIcon, + [COMPONENT_ICON_TYPES.discussion]: QuestionAnswerOutlineIcon, + [COMPONENT_ICON_TYPES.library]: LibraryIcon, + [COMPONENT_ICON_TYPES.html]: TextFieldsIcon, + [COMPONENT_ICON_TYPES.openassessment]: EditNoteIcon, + [COMPONENT_ICON_TYPES.problem]: HelpOutlineIcon, + [COMPONENT_ICON_TYPES.video]: VideoCameraIcon, + [COMPONENT_ICON_TYPES.dragAndDrop]: BackHandIcon, +}; diff --git a/src/course-unit/data/api.js b/src/course-unit/data/api.js index 47be488696..6352c121dc 100644 --- a/src/course-unit/data/api.js +++ b/src/course-unit/data/api.js @@ -14,6 +14,7 @@ const getStudioBaseUrl = () => getConfig().STUDIO_BASE_URL; const getLmsBaseUrl = () => getConfig().LMS_BASE_URL; export const getCourseUnitApiUrl = (itemId) => `${getStudioBaseUrl()}/xblock/container/${itemId}`; +export const postXBlockBaseApiUrl = () => `${getStudioBaseUrl()}/xblock/`; export const getXBlockBaseApiUrl = (itemId) => `${getStudioBaseUrl()}/xblock/${itemId}`; export const getCourseSectionVerticalApiUrl = (itemId) => `${getStudioBaseUrl()}/api/contentstore/v1/container_handler/${itemId}`; export const getSequenceMetadataApiUrl = (sequenceId) => `${getLmsBaseUrl()}/api/courseware/sequence/${sequenceId}`; @@ -112,3 +113,15 @@ export async function getCourseHomeCourseMetadata(courseId, rootSlug) { return normalizeCourseHomeCourseMetadata(data, rootSlug); } + +export async function createCourseXblock({ type, category, parentLocator }) { + const body = { + type, + category: category || type, + parent_locator: parentLocator, + }; + const { data } = await getAuthenticatedHttpClient() + .post(postXBlockBaseApiUrl(), body); + + return data; +} diff --git a/src/course-unit/data/slice.js b/src/course-unit/data/slice.js index c80fa02b39..8708704025 100644 --- a/src/course-unit/data/slice.js +++ b/src/course-unit/data/slice.js @@ -67,6 +67,12 @@ const slice = createSlice({ courseSectionVerticalLoadingStatus: payload.status, }; }, + updateLoadingCourseXblockStatus: (state, { payload }) => { + state.loadingStatus = { + ...state.loadingStatus, + createUnitXblockLoadingStatus: payload.status, + }; + }, }, }); @@ -84,6 +90,7 @@ export const { fetchCourseDenied, fetchCourseSectionVerticalDataSuccess, updateLoadingCourseSectionVerticalDataStatus, + updateLoadingCourseXblockStatus, } = slice.actions; export const { diff --git a/src/course-unit/data/thunk.js b/src/course-unit/data/thunk.js index 883c53b99e..6b3cc611f3 100644 --- a/src/course-unit/data/thunk.js +++ b/src/course-unit/data/thunk.js @@ -14,7 +14,10 @@ import { editUnitDisplayName, getSequenceMetadata, getCourseMetadata, - getLearningSequencesOutline, getCourseHomeCourseMetadata, getCourseSectionVerticalData, + getLearningSequencesOutline, + getCourseHomeCourseMetadata, + getCourseSectionVerticalData, + createCourseXblock, } from './api'; import { updateLoadingCourseUnitStatus, @@ -29,6 +32,7 @@ import { fetchCourseFailure, fetchCourseSectionVerticalDataSuccess, updateLoadingCourseSectionVerticalDataStatus, + updateLoadingCourseXblockStatus, } from './slice'; export function fetchCourseUnitQuery(courseId) { @@ -211,3 +215,29 @@ export function fetchCourse(courseId) { }); }; } + +export function createNewCourseXblock(body, callback) { + return async (dispatch) => { + dispatch(updateLoadingCourseXblockStatus({ status: RequestStatus.IN_PROGRESS })); + dispatch(showProcessingNotification(NOTIFICATION_MESSAGES.adding)); + dispatch(updateSavingStatus({ status: RequestStatus.PENDING })); + + try { + await createCourseXblock(body).then(async (result) => { + if (result) { + // ToDo: implement fetching (update) xblocks after success creating + dispatch(hideProcessingNotification()); + dispatch(updateLoadingCourseXblockStatus({ status: RequestStatus.SUCCESSFUL })); + dispatch(updateSavingStatus({ status: RequestStatus.SUCCESSFUL })); + if (callback) { + callback(result); + } + } + }); + } catch (error) { + dispatch(hideProcessingNotification()); + dispatch(updateLoadingCourseXblockStatus({ status: RequestStatus.FAILED })); + dispatch(updateSavingStatus({ status: RequestStatus.FAILED })); + } + }; +} diff --git a/src/course-unit/hooks.jsx b/src/course-unit/hooks.jsx index 3ecb42a28f..905be41d71 100644 --- a/src/course-unit/hooks.jsx +++ b/src/course-unit/hooks.jsx @@ -5,6 +5,7 @@ import { useNavigate } from 'react-router-dom'; import { RequestStatus } from '../data/constants'; import { + createNewCourseXblock, fetchCourseUnitQuery, editCourseItemQuery, fetchSequence, @@ -66,6 +67,10 @@ export const useCourseUnit = ({ courseId, blockId }) => { } }; + const handleCreateNewCourseXblock = (body, callback) => ( + dispatch(createNewCourseXblock(body, callback)) + ); + useEffect(() => { dispatch(fetchCourseUnitQuery(blockId)); dispatch(fetchCourseSectionVerticalData(blockId)); @@ -85,5 +90,6 @@ export const useCourseUnit = ({ courseId, blockId }) => { headerNavigationsActions, handleTitleEdit, handleTitleEditSubmit, + handleCreateNewCourseXblock, }; }; diff --git a/src/i18n/messages/ar.json b/src/i18n/messages/ar.json index 393a9019a3..0c24ccbd7f 100644 --- a/src/i18n/messages/ar.json +++ b/src/i18n/messages/ar.json @@ -989,5 +989,7 @@ "course-authoring.course-unit.sequence-nav-label-text": "Sequence navigation", "course-authoring.course-unit.sequence.load.failure": "There was an error loading this course.", "course-authoring.course-unit.sequence.no.content": "There is no content here.", - "course-authoring.course-unit.sequence.navigation.menu": "{current} of {total}" + "course-authoring.course-unit.sequence.navigation.menu": "{current} of {total}", + "course-authoring.course-unit.add.component.title": "Add a new component", + "course-authoring.course-unit.add.component.button.text": "Add Component:" } diff --git a/src/i18n/messages/de.json b/src/i18n/messages/de.json index 09c7da1eb7..2500a14407 100644 --- a/src/i18n/messages/de.json +++ b/src/i18n/messages/de.json @@ -989,5 +989,7 @@ "course-authoring.course-unit.sequence-nav-label-text": "Sequence navigation", "course-authoring.course-unit.sequence.load.failure": "There was an error loading this course.", "course-authoring.course-unit.sequence.no.content": "There is no content here.", - "course-authoring.course-unit.sequence.navigation.menu": "{current} of {total}" + "course-authoring.course-unit.sequence.navigation.menu": "{current} of {total}", + "course-authoring.course-unit.add.component.title": "Add a new component", + "course-authoring.course-unit.add.component.button.text": "Add Component:" } diff --git a/src/i18n/messages/de_DE.json b/src/i18n/messages/de_DE.json index 0286fe7a06..de185d7071 100644 --- a/src/i18n/messages/de_DE.json +++ b/src/i18n/messages/de_DE.json @@ -989,5 +989,7 @@ "course-authoring.course-unit.sequence-nav-label-text": "Sequence navigation", "course-authoring.course-unit.sequence.load.failure": "There was an error loading this course.", "course-authoring.course-unit.sequence.no.content": "There is no content here.", - "course-authoring.course-unit.sequence.navigation.menu": "{current} of {total}" + "course-authoring.course-unit.sequence.navigation.menu": "{current} of {total}", + "course-authoring.course-unit.add.component.title": "Add a new component", + "course-authoring.course-unit.add.component.button.text": "Add Component:" } diff --git a/src/i18n/messages/es_419.json b/src/i18n/messages/es_419.json index c59fa68499..7999fe94ae 100644 --- a/src/i18n/messages/es_419.json +++ b/src/i18n/messages/es_419.json @@ -989,5 +989,7 @@ "course-authoring.course-unit.sequence-nav-label-text": "Sequence navigation", "course-authoring.course-unit.sequence.load.failure": "There was an error loading this course.", "course-authoring.course-unit.sequence.no.content": "There is no content here.", - "course-authoring.course-unit.sequence.navigation.menu": "{current} of {total}" + "course-authoring.course-unit.sequence.navigation.menu": "{current} of {total}", + "course-authoring.course-unit.add.component.title": "Add a new component", + "course-authoring.course-unit.add.component.button.text": "Add Component:" } diff --git a/src/i18n/messages/fa_IR.json b/src/i18n/messages/fa_IR.json index a8ef072f6d..bafa9a9fb7 100644 --- a/src/i18n/messages/fa_IR.json +++ b/src/i18n/messages/fa_IR.json @@ -12,5 +12,7 @@ "course-authoring.course-unit.sequence-nav-label-text": "Sequence navigation", "course-authoring.course-unit.sequence.load.failure": "There was an error loading this course.", "course-authoring.course-unit.sequence.no.content": "There is no content here.", - "course-authoring.course-unit.sequence.navigation.menu": "{current} of {total}" + "course-authoring.course-unit.sequence.navigation.menu": "{current} of {total}", + "course-authoring.course-unit.add.component.title": "Add a new component", + "course-authoring.course-unit.add.component.button.text": "Add Component:" } diff --git a/src/i18n/messages/fr.json b/src/i18n/messages/fr.json index e91037042e..da68eb6719 100644 --- a/src/i18n/messages/fr.json +++ b/src/i18n/messages/fr.json @@ -989,5 +989,7 @@ "course-authoring.course-unit.sequence-nav-label-text": "Sequence navigation", "course-authoring.course-unit.sequence.load.failure": "There was an error loading this course.", "course-authoring.course-unit.sequence.no.content": "There is no content here.", - "course-authoring.course-unit.sequence.navigation.menu": "{current} of {total}" + "course-authoring.course-unit.sequence.navigation.menu": "{current} of {total}", + "course-authoring.course-unit.add.component.title": "Add a new component", + "course-authoring.course-unit.add.component.button.text": "Add Component:" } diff --git a/src/i18n/messages/fr_CA.json b/src/i18n/messages/fr_CA.json index f3251fbbb0..ca1e8b4152 100644 --- a/src/i18n/messages/fr_CA.json +++ b/src/i18n/messages/fr_CA.json @@ -989,5 +989,7 @@ "course-authoring.course-unit.sequence-nav-label-text": "Sequence navigation", "course-authoring.course-unit.sequence.load.failure": "There was an error loading this course.", "course-authoring.course-unit.sequence.no.content": "There is no content here.", - "course-authoring.course-unit.sequence.navigation.menu": "{current} of {total}" + "course-authoring.course-unit.sequence.navigation.menu": "{current} of {total}", + "course-authoring.course-unit.add.component.title": "Add a new component", + "course-authoring.course-unit.add.component.button.text": "Add Component:" } diff --git a/src/i18n/messages/hi.json b/src/i18n/messages/hi.json index 09c7da1eb7..2500a14407 100644 --- a/src/i18n/messages/hi.json +++ b/src/i18n/messages/hi.json @@ -989,5 +989,7 @@ "course-authoring.course-unit.sequence-nav-label-text": "Sequence navigation", "course-authoring.course-unit.sequence.load.failure": "There was an error loading this course.", "course-authoring.course-unit.sequence.no.content": "There is no content here.", - "course-authoring.course-unit.sequence.navigation.menu": "{current} of {total}" + "course-authoring.course-unit.sequence.navigation.menu": "{current} of {total}", + "course-authoring.course-unit.add.component.title": "Add a new component", + "course-authoring.course-unit.add.component.button.text": "Add Component:" } diff --git a/src/i18n/messages/it.json b/src/i18n/messages/it.json index 09c7da1eb7..2500a14407 100644 --- a/src/i18n/messages/it.json +++ b/src/i18n/messages/it.json @@ -989,5 +989,7 @@ "course-authoring.course-unit.sequence-nav-label-text": "Sequence navigation", "course-authoring.course-unit.sequence.load.failure": "There was an error loading this course.", "course-authoring.course-unit.sequence.no.content": "There is no content here.", - "course-authoring.course-unit.sequence.navigation.menu": "{current} of {total}" + "course-authoring.course-unit.sequence.navigation.menu": "{current} of {total}", + "course-authoring.course-unit.add.component.title": "Add a new component", + "course-authoring.course-unit.add.component.button.text": "Add Component:" } diff --git a/src/i18n/messages/it_IT.json b/src/i18n/messages/it_IT.json index 21f4b2660f..333f18723d 100644 --- a/src/i18n/messages/it_IT.json +++ b/src/i18n/messages/it_IT.json @@ -989,5 +989,7 @@ "course-authoring.course-unit.sequence-nav-label-text": "Sequence navigation", "course-authoring.course-unit.sequence.load.failure": "There was an error loading this course.", "course-authoring.course-unit.sequence.no.content": "There is no content here.", - "course-authoring.course-unit.sequence.navigation.menu": "{current} of {total}" + "course-authoring.course-unit.sequence.navigation.menu": "{current} of {total}", + "course-authoring.course-unit.add.component.title": "Add a new component", + "course-authoring.course-unit.add.component.button.text": "Add Component:" } diff --git a/src/i18n/messages/pt.json b/src/i18n/messages/pt.json index 09c7da1eb7..2500a14407 100644 --- a/src/i18n/messages/pt.json +++ b/src/i18n/messages/pt.json @@ -989,5 +989,7 @@ "course-authoring.course-unit.sequence-nav-label-text": "Sequence navigation", "course-authoring.course-unit.sequence.load.failure": "There was an error loading this course.", "course-authoring.course-unit.sequence.no.content": "There is no content here.", - "course-authoring.course-unit.sequence.navigation.menu": "{current} of {total}" + "course-authoring.course-unit.sequence.navigation.menu": "{current} of {total}", + "course-authoring.course-unit.add.component.title": "Add a new component", + "course-authoring.course-unit.add.component.button.text": "Add Component:" } diff --git a/src/i18n/messages/pt_PT.json b/src/i18n/messages/pt_PT.json index b447c987fd..c281896c81 100644 --- a/src/i18n/messages/pt_PT.json +++ b/src/i18n/messages/pt_PT.json @@ -989,5 +989,7 @@ "course-authoring.course-unit.sequence-nav-label-text": "Sequence navigation", "course-authoring.course-unit.sequence.load.failure": "There was an error loading this course.", "course-authoring.course-unit.sequence.no.content": "There is no content here.", - "course-authoring.course-unit.sequence.navigation.menu": "{current} of {total}" + "course-authoring.course-unit.sequence.navigation.menu": "{current} of {total}", + "course-authoring.course-unit.add.component.title": "Add a new component", + "course-authoring.course-unit.add.component.button.text": "Add Component:" } diff --git a/src/i18n/messages/ru.json b/src/i18n/messages/ru.json index 09c7da1eb7..2500a14407 100644 --- a/src/i18n/messages/ru.json +++ b/src/i18n/messages/ru.json @@ -989,5 +989,7 @@ "course-authoring.course-unit.sequence-nav-label-text": "Sequence navigation", "course-authoring.course-unit.sequence.load.failure": "There was an error loading this course.", "course-authoring.course-unit.sequence.no.content": "There is no content here.", - "course-authoring.course-unit.sequence.navigation.menu": "{current} of {total}" + "course-authoring.course-unit.sequence.navigation.menu": "{current} of {total}", + "course-authoring.course-unit.add.component.title": "Add a new component", + "course-authoring.course-unit.add.component.button.text": "Add Component:" } diff --git a/src/i18n/messages/uk.json b/src/i18n/messages/uk.json index 09c7da1eb7..2500a14407 100644 --- a/src/i18n/messages/uk.json +++ b/src/i18n/messages/uk.json @@ -989,5 +989,7 @@ "course-authoring.course-unit.sequence-nav-label-text": "Sequence navigation", "course-authoring.course-unit.sequence.load.failure": "There was an error loading this course.", "course-authoring.course-unit.sequence.no.content": "There is no content here.", - "course-authoring.course-unit.sequence.navigation.menu": "{current} of {total}" + "course-authoring.course-unit.sequence.navigation.menu": "{current} of {total}", + "course-authoring.course-unit.add.component.title": "Add a new component", + "course-authoring.course-unit.add.component.button.text": "Add Component:" } diff --git a/src/i18n/messages/zh_CN.json b/src/i18n/messages/zh_CN.json index 09c7da1eb7..2500a14407 100644 --- a/src/i18n/messages/zh_CN.json +++ b/src/i18n/messages/zh_CN.json @@ -989,5 +989,7 @@ "course-authoring.course-unit.sequence-nav-label-text": "Sequence navigation", "course-authoring.course-unit.sequence.load.failure": "There was an error loading this course.", "course-authoring.course-unit.sequence.no.content": "There is no content here.", - "course-authoring.course-unit.sequence.navigation.menu": "{current} of {total}" + "course-authoring.course-unit.sequence.navigation.menu": "{current} of {total}", + "course-authoring.course-unit.add.component.title": "Add a new component", + "course-authoring.course-unit.add.component.button.text": "Add Component:" } diff --git a/src/utils.js b/src/utils.js index 8178e754eb..fa6f303372 100644 --- a/src/utils.js +++ b/src/utils.js @@ -4,7 +4,7 @@ import { useMediaQuery } from 'react-responsive'; import * as Yup from 'yup'; import { snakeCase } from 'lodash/string'; import moment from 'moment'; -import { getConfig } from '@edx/frontend-platform'; +import { getConfig, getPath } from '@edx/frontend-platform'; import { RequestStatus } from './data/constants'; import { getCourseAppSettingValue, getLoadingStatus } from './pages-and-resources/data/selectors'; @@ -268,3 +268,22 @@ export const getFileSizeToClosestByte = (fileSize) => { const fileSizeFixedDecimal = Number.parseFloat(size).toFixed(2); return `${fileSizeFixedDecimal} ${units[divides]}`; }; + +/** + * Create a correct inner path depend on config PUBLIC_PATH. + * @param {string} checkPath - the internal route path that is validated + * @returns {string} - the correct internal route path + */ +export const createCorrectInternalRoute = (checkPath) => { + let basePath = getPath(getConfig().PUBLIC_PATH); + + if (basePath.endsWith('/')) { + basePath = basePath.slice(0, -1); + } + + if (!checkPath.startsWith(basePath)) { + return `${basePath}${checkPath}`; + } + + return checkPath; +}; diff --git a/src/utils.test.js b/src/utils.test.js index 3c0ddabf7f..e4aada849f 100644 --- a/src/utils.test.js +++ b/src/utils.test.js @@ -1,4 +1,12 @@ -import { getFileSizeToClosestByte } from './utils'; +import { getConfig, getPath } from '@edx/frontend-platform'; + +import { getFileSizeToClosestByte, createCorrectInternalRoute } from './utils'; + +jest.mock('@edx/frontend-platform', () => ({ + getConfig: jest.fn(), + ensureConfig: jest.fn(), + getPath: jest.fn(), +})); describe('FilesAndUploads utils', () => { describe('getFileSizeToClosestByte', () => { @@ -33,4 +41,40 @@ describe('FilesAndUploads utils', () => { expect(expectedSize).toEqual(actualSize); }); }); + describe('createCorrectInternalRoute', () => { + beforeEach(() => { + getConfig.mockReset(); + getPath.mockReset(); + }); + + it('returns the correct internal route when checkPath is not prefixed with basePath', () => { + getConfig.mockReturnValue({ PUBLIC_PATH: 'example.com' }); + getPath.mockReturnValue('/'); + + const checkPath = '/some/path'; + const result = createCorrectInternalRoute(checkPath); + + expect(result).toBe('/some/path'); + }); + + it('returns the input checkPath when it is already prefixed with basePath', () => { + getConfig.mockReturnValue({ PUBLIC_PATH: 'example.com' }); + getPath.mockReturnValue('/course-authoring'); + + const checkPath = '/course-authoring/some/path'; + const result = createCorrectInternalRoute(checkPath); + + expect(result).toBe('/course-authoring/some/path'); + }); + + it('handles basePath ending with a slash correctly', () => { + getConfig.mockReturnValue({ PUBLIC_PATH: 'example.com/' }); + getPath.mockReturnValue('/course-authoring/'); + + const checkPath = '/some/path'; + const result = createCorrectInternalRoute(checkPath); + + expect(result).toBe('/course-authoring/some/path'); + }); + }); });