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');
+ });
+ });
});