From 9c52b8b6c5d68bed51a5d96e7c893037ad4c7caf Mon Sep 17 00:00:00 2001 From: Ihor Romaniuk Date: Mon, 5 Feb 2024 17:58:35 +0100 Subject: [PATCH] feat: [FC-0044] Unit page header section (#808) * feat: create Unit page and add page header functionality * fix: after code review --------- Co-authored-by: monteri --- .env | 1 + .env.development | 2 +- .env.test | 1 + src/CourseAuthoringRoutes.jsx | 4 +- src/course-outline/CourseOutline.jsx | 4 +- src/course-outline/CourseOutline.test.jsx | 1 - src/course-outline/card-header/CardHeader.jsx | 25 +- .../card-header/CardHeader.test.jsx | 23 +- src/course-outline/hooks.jsx | 2 +- .../section-card/SectionCard.jsx | 2 +- .../subsection-card/SubsectionCard.jsx | 8 +- .../subsection-card/SubsectionCard.test.jsx | 64 +- src/course-unit/CourseUnit.jsx | 107 ++ src/course-unit/CourseUnit.scss | 1 + src/course-unit/CourseUnit.test.jsx | 149 +++ src/course-unit/__mocks__/courseUnitIndex.js | 1123 +++++++++++++++++ src/course-unit/__mocks__/index.js | 2 + src/course-unit/breadcrumbs/Breadcrumbs.jsx | 107 ++ src/course-unit/breadcrumbs/Breadcrumbs.scss | 11 + .../breadcrumbs/Breadcrumbs.test.jsx | 106 ++ src/course-unit/breadcrumbs/messages.js | 15 + src/course-unit/data/api.js | 38 + src/course-unit/data/selectors.js | 5 + src/course-unit/data/slice.js | 39 + src/course-unit/data/thunk.js | 49 + .../header-navigations/HeaderNavigations.jsx | 36 + .../HeaderNavigations.test.jsx | 42 + .../header-navigations/messages.js | 14 + src/course-unit/header-title/HeaderTitle.jsx | 67 + .../header-title/HeaderTitle.test.jsx | 69 + src/course-unit/header-title/messages.js | 18 + src/course-unit/hooks.jsx | 75 ++ src/course-unit/index.js | 2 + src/course-unit/messages.js | 10 + src/course-unit/utils.jsx | 23 + src/generic/sub-header/SubHeader.jsx | 18 +- src/generic/sub-header/SubHeader.scss | 4 +- src/i18n/messages/ar.json | 9 +- src/i18n/messages/de.json | 9 +- src/i18n/messages/de_DE.json | 9 +- src/i18n/messages/es_419.json | 9 +- src/i18n/messages/fa_IR.json | 10 +- src/i18n/messages/fr.json | 9 +- src/i18n/messages/fr_CA.json | 9 +- src/i18n/messages/hi.json | 9 +- src/i18n/messages/it.json | 9 +- src/i18n/messages/it_IT.json | 9 +- src/i18n/messages/pt.json | 9 +- src/i18n/messages/pt_PT.json | 9 +- src/i18n/messages/ru.json | 9 +- src/i18n/messages/uk.json | 9 +- src/i18n/messages/zh_CN.json | 9 +- src/index.jsx | 1 + src/setupTest.js | 1 + src/store.js | 2 + 55 files changed, 2347 insertions(+), 60 deletions(-) create mode 100644 src/course-unit/CourseUnit.jsx create mode 100644 src/course-unit/CourseUnit.scss create mode 100644 src/course-unit/CourseUnit.test.jsx create mode 100644 src/course-unit/__mocks__/courseUnitIndex.js create mode 100644 src/course-unit/__mocks__/index.js create mode 100644 src/course-unit/breadcrumbs/Breadcrumbs.jsx create mode 100644 src/course-unit/breadcrumbs/Breadcrumbs.scss create mode 100644 src/course-unit/breadcrumbs/Breadcrumbs.test.jsx create mode 100644 src/course-unit/breadcrumbs/messages.js create mode 100644 src/course-unit/data/api.js create mode 100644 src/course-unit/data/selectors.js create mode 100644 src/course-unit/data/slice.js create mode 100644 src/course-unit/data/thunk.js create mode 100644 src/course-unit/header-navigations/HeaderNavigations.jsx create mode 100644 src/course-unit/header-navigations/HeaderNavigations.test.jsx create mode 100644 src/course-unit/header-navigations/messages.js create mode 100644 src/course-unit/header-title/HeaderTitle.jsx create mode 100644 src/course-unit/header-title/HeaderTitle.test.jsx create mode 100644 src/course-unit/header-title/messages.js create mode 100644 src/course-unit/hooks.jsx create mode 100644 src/course-unit/index.js create mode 100644 src/course-unit/messages.js create mode 100644 src/course-unit/utils.jsx diff --git a/.env b/.env index 9c900c37fb..d5cb397aad 100644 --- a/.env +++ b/.env @@ -9,6 +9,7 @@ EXAMS_BASE_URL='' FAVICON_URL='' LANGUAGE_PREFERENCE_COOKIE_NAME='' LMS_BASE_URL='' +PREVIEW_BASE_URL='' LEARNING_BASE_URL='' LOGIN_URL='' LOGO_TRADEMARK_URL='' diff --git a/.env.development b/.env.development index 55b8ce70cd..bb636463c7 100644 --- a/.env.development +++ b/.env.development @@ -34,7 +34,6 @@ ENABLE_PROGRESS_GRAPH_SETTINGS=false ENABLE_TEAM_TYPE_SETTING=false ENABLE_NEW_EDITOR_PAGES=true ENABLE_NEW_VIDEO_UPLOAD_PAGE = false -ENABLE_UNIT_PAGE = false ENABLE_VIDEO_UPLOAD_PAGE_LINK_IN_CONTENT_DROPDOWN = false ENABLE_TAGGING_TAXONOMY_PAGES = true BBB_LEARN_MORE_URL='' @@ -43,3 +42,4 @@ HOTJAR_VERSION=6 HOTJAR_DEBUG=true INVITE_STUDENTS_EMAIL_TO="someone@domain.com" AI_TRANSLATIONS_BASE_URL='http://localhost:18760' +PREVIEW_BASE_URL='http://preview.localhost:18000' diff --git a/.env.test b/.env.test index 8c810bb90b..0689cddb3b 100644 --- a/.env.test +++ b/.env.test @@ -8,6 +8,7 @@ EXAMS_BASE_URL= FAVICON_URL='https://edx-cdn.org/v3/default/favicon.ico' LANGUAGE_PREFERENCE_COOKIE_NAME='openedx-language-preference' LMS_BASE_URL='http://localhost:18000' +PREVIEW_BASE_URL='http://preview.localhost:18000' LEARNING_BASE_URL='http://localhost:2000' LOGIN_URL='http://localhost:18000/login' LOGO_TRADEMARK_URL=https://edx-cdn.org/v3/default/logo-trademark.svg diff --git a/src/CourseAuthoringRoutes.jsx b/src/CourseAuthoringRoutes.jsx index 6ff7f475bd..f5531eafa4 100644 --- a/src/CourseAuthoringRoutes.jsx +++ b/src/CourseAuthoringRoutes.jsx @@ -3,7 +3,6 @@ import { Navigate, Routes, Route, useParams, } from 'react-router-dom'; import { PageWrap } from '@edx/frontend-platform/react'; -import Placeholder from '@edx/frontend-lib-content-components'; import CourseAuthoringPage from './CourseAuthoringPage'; import { PagesAndResources } from './pages-and-resources'; import EditorContainer from './editors/EditorContainer'; @@ -16,6 +15,7 @@ import ScheduleAndDetails from './schedule-and-details'; import { GradingSettings } from './grading-settings'; import CourseTeam from './course-team/CourseTeam'; import { CourseUpdates } from './course-updates'; +import { CourseUnit } from './course-unit'; import CourseExportPage from './export-page/CourseExportPage'; import CourseImportPage from './import-page/CourseImportPage'; @@ -71,7 +71,7 @@ const CourseAuthoringRoutes = () => { /> : null} + element={} /> { const intl = useIntl(); + const [searchParams] = useSearchParams(); const [titleValue, setTitleValue] = useState(title); + const cardHeaderRef = useRef(null); const isDisabledPublish = (status === ITEM_BADGE_STATUS.live || status === ITEM_BADGE_STATUS.publishedNotLive) && !hasChanges; + useEffect(() => { + const locatorId = searchParams.get('show'); + if (!locatorId) { + return; + } + + if (cardHeaderRef.current && locatorId === cardId) { + scrollToElement(cardHeaderRef.current); + } + }, []); + useEscapeClick({ onEscape: () => { setTitleValue(title); @@ -51,7 +67,11 @@ const CardHeader = ({ }); return ( -
+
{isFormOpen ? ( { +const renderComponent = (props, entry = '/') => { const titleComponent = ( { return render( - + + + , , ); }; diff --git a/src/course-outline/hooks.jsx b/src/course-outline/hooks.jsx index 6604357129..79d99db93d 100644 --- a/src/course-outline/hooks.jsx +++ b/src/course-outline/hooks.jsx @@ -1,7 +1,7 @@ import { useEffect, useState } from 'react'; import { useDispatch, useSelector } from 'react-redux'; -import { useToggle } from '@edx/paragon'; import { useNavigate } from 'react-router-dom'; +import { useToggle } from '@edx/paragon'; import { getConfig } from '@edx/frontend-platform'; import { RequestStatus } from '../data/constants'; diff --git a/src/course-outline/section-card/SectionCard.jsx b/src/course-outline/section-card/SectionCard.jsx index 9787c0a823..c444d9db19 100644 --- a/src/course-outline/section-card/SectionCard.jsx +++ b/src/course-outline/section-card/SectionCard.jsx @@ -152,7 +152,7 @@ const SectionCard = ({
{isHeaderVisible && ( ({ + ...jest.requireActual('react-router-dom'), + useLocation: () => ({ + pathname: mockPathname, + }), +})); const section = { id: '123', @@ -42,28 +51,30 @@ const subsection = { const onEditSubectionSubmit = jest.fn(); -const renderComponent = (props) => render( - - - - children - - , +const renderComponent = (props, entry = '/') => render( + + + + + children + + , + , , ); @@ -197,4 +208,13 @@ describe('', () => { }); expect(await findByText(cardHeaderMessages.statusBadgeDraft.defaultMessage)).toBeInTheDocument(); }); + + it('check extended section when URL has a "show" param', async () => { + const { findByTestId } = renderComponent(null, `?show=${section.id}`); + + const cardUnits = await findByTestId('subsection-card__units'); + const newUnitButton = await findByTestId('new-unit-button'); + expect(cardUnits).toBeInTheDocument(); + expect(newUnitButton).toBeInTheDocument(); + }); }); diff --git a/src/course-unit/CourseUnit.jsx b/src/course-unit/CourseUnit.jsx new file mode 100644 index 0000000000..6a6979704a --- /dev/null +++ b/src/course-unit/CourseUnit.jsx @@ -0,0 +1,107 @@ +import PropTypes from 'prop-types'; +import { useSelector } from 'react-redux'; +import { useParams } from 'react-router-dom'; +import { Container, Layout } from '@edx/paragon'; +import { useIntl, injectIntl } from '@edx/frontend-platform/i18n'; +import { ErrorAlert } from '@edx/frontend-lib-content-components'; + +import { getProcessingNotification } from '../generic/processing-notification/data/selectors'; +import SubHeader from '../generic/sub-header/SubHeader'; +import { RequestStatus } from '../data/constants'; +import getPageHeadTitle from '../generic/utils'; +import ProcessingNotification from '../generic/processing-notification'; +import InternetConnectionAlert from '../generic/internet-connection-alert'; +import HeaderTitle from './header-title/HeaderTitle'; +import Breadcrumbs from './breadcrumbs/Breadcrumbs'; +import HeaderNavigations from './header-navigations/HeaderNavigations'; +import { useCourseUnit } from './hooks'; +import messages from './messages'; + +import './CourseUnit.scss'; + +const CourseUnit = ({ courseId }) => { + const { blockId } = useParams(); + const intl = useIntl(); + const { + isLoading, + unitTitle, + savingStatus, + isTitleEditFormOpen, + isInternetConnectionAlertFailed, + handleTitleEditSubmit, + headerNavigationsActions, + handleTitleEdit, + handleInternetConnectionFailed, + } = useCourseUnit({ courseId, blockId }); + + document.title = getPageHeadTitle('', unitTitle); + + const { + isShow: isShowProcessingNotification, + title: processingNotificationTitle, + } = useSelector(getProcessingNotification); + + if (isLoading) { + return null; + } + + return ( + <> + +
+ + {intl.formatMessage(messages.alertFailedGeneric, { actionName: 'save', type: 'changes' })} + + + )} + breadcrumbs={( + + )} + headerActions={( + + )} + /> + + + + +
+
+
+ + +
+ + ); +}; + +CourseUnit.propTypes = { + courseId: PropTypes.string.isRequired, +}; + +export default injectIntl(CourseUnit); diff --git a/src/course-unit/CourseUnit.scss b/src/course-unit/CourseUnit.scss new file mode 100644 index 0000000000..82ba56f504 --- /dev/null +++ b/src/course-unit/CourseUnit.scss @@ -0,0 +1 @@ +@import "./breadcrumbs/Breadcrumbs"; diff --git a/src/course-unit/CourseUnit.test.jsx b/src/course-unit/CourseUnit.test.jsx new file mode 100644 index 0000000000..bc4e1ad9a6 --- /dev/null +++ b/src/course-unit/CourseUnit.test.jsx @@ -0,0 +1,149 @@ +import MockAdapter from 'axios-mock-adapter'; +import { + act, render, waitFor, fireEvent, +} from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import { IntlProvider } from '@edx/frontend-platform/i18n'; +import { AppProvider } from '@edx/frontend-platform/react'; +import { getConfig, initializeMockApp } from '@edx/frontend-platform'; +import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth'; + +import { + getCourseUnitApiUrl, + getXBlockBaseApiUrl, +} from './data/api'; +import { + fetchCourseUnitQuery, +} from './data/thunk'; +import initializeStore from '../store'; +import { + courseUnitIndexMock, +} from './__mocks__'; +import { executeThunk } from '../utils'; +import CourseUnit from './CourseUnit'; +import headerNavigationsMessages from './header-navigations/messages'; +import headerTitleMessages from './header-title/messages'; +import { getUnitPreviewPath, getUnitViewLivePath } from './utils'; + +let axiosMock; +let store; +const courseId = '123'; +const sectionId = 'graded_interactions'; +const subsectionId = '19a30717eff543078a5d94ae9d6c18a5'; +const blockId = '567890'; +const unitDisplayName = courseUnitIndexMock.metadata.display_name; + +jest.mock('react-router-dom', () => ({ + ...jest.requireActual('react-router-dom'), + useParams: () => ({ blockId }), +})); + +const RootWrapper = () => ( + + + + + +); + +describe('', () => { + beforeEach(async () => { + initializeMockApp({ + authenticatedUser: { + userId: 3, + username: 'abc123', + administrator: true, + roles: [], + }, + }); + + store = initializeStore(); + axiosMock = new MockAdapter(getAuthenticatedHttpClient()); + axiosMock + .onGet(getCourseUnitApiUrl(courseId)) + .reply(200, courseUnitIndexMock); + await executeThunk(fetchCourseUnitQuery(courseId), store.dispatch); + }); + + it('render CourseUnit component correctly', async () => { + const { getByText, getByRole } = render(); + const currentSectionName = courseUnitIndexMock.ancestor_info.ancestors[1].display_name; + const currentSubSectionName = courseUnitIndexMock.ancestor_info.ancestors[1].display_name; + + await waitFor(() => { + expect(getByText(unitDisplayName)).toBeInTheDocument(); + expect(getByRole('button', { name: headerTitleMessages.altButtonEdit.defaultMessage })).toBeInTheDocument(); + expect(getByRole('button', { name: headerTitleMessages.altButtonSettings.defaultMessage })).toBeInTheDocument(); + expect(getByRole('button', { name: headerNavigationsMessages.viewLiveButton.defaultMessage })).toBeInTheDocument(); + expect(getByRole('button', { name: headerNavigationsMessages.previewButton.defaultMessage })).toBeInTheDocument(); + expect(getByRole('button', { name: currentSectionName })).toBeInTheDocument(); + expect(getByRole('button', { name: currentSubSectionName })).toBeInTheDocument(); + }); + }); + + it('handles CourseUnit header action buttons', async () => { + const { open } = window; + window.open = jest.fn(); + const { getByRole } = render(); + + await waitFor(() => { + const viewLiveButton = getByRole('button', { name: headerNavigationsMessages.viewLiveButton.defaultMessage }); + userEvent.click(viewLiveButton); + expect(window.open).toHaveBeenCalled(); + const VIEW_LIVE_LINK = getConfig().LMS_BASE_URL + getUnitViewLivePath(courseId, blockId); + expect(window.open).toHaveBeenCalledWith(VIEW_LIVE_LINK, '_blank'); + + const previewButton = getByRole('button', { name: headerNavigationsMessages.previewButton.defaultMessage }); + userEvent.click(previewButton); + expect(window.open).toHaveBeenCalled(); + // eslint-disable-next-line max-len + const PREVIEW_LINK = getConfig().PREVIEW_BASE_URL + getUnitPreviewPath(courseId, sectionId, subsectionId, blockId); + expect(window.open).toHaveBeenCalledWith(PREVIEW_LINK, '_blank'); + }); + + window.open = open; + }); + + it('checks courseUnit title changing when edit query is successfully', async () => { + const { + findByText, queryByRole, getByRole, + } = render(); + let editTitleButton = null; + let titleEditField = null; + const newDisplayName = `${unitDisplayName} new`; + + axiosMock + .onPost(getXBlockBaseApiUrl(blockId, { + metadata: { + display_name: newDisplayName, + }, + })) + .reply(200, { dummy: 'value' }); + axiosMock + .onGet(getCourseUnitApiUrl(blockId)) + .reply(200, { + ...courseUnitIndexMock, + metadata: { + ...courseUnitIndexMock.metadata, + display_name: newDisplayName, + }, + }); + + await waitFor(() => { + editTitleButton = getByRole('button', { name: headerTitleMessages.altButtonEdit.defaultMessage }); + titleEditField = queryByRole('textbox', { name: headerTitleMessages.ariaLabelButtonEdit.defaultMessage }); + }); + expect(titleEditField).not.toBeInTheDocument(); + fireEvent.click(editTitleButton); + titleEditField = getByRole('textbox', { name: headerTitleMessages.ariaLabelButtonEdit.defaultMessage }); + fireEvent.change(titleEditField, { target: { value: newDisplayName } }); + await act(async () => { + fireEvent.blur(titleEditField); + }); + expect(titleEditField).toHaveValue(newDisplayName); + + titleEditField = queryByRole('textbox', { name: headerTitleMessages.ariaLabelButtonEdit.defaultMessage }); + expect(titleEditField).not.toBeInTheDocument(); + expect(await findByText(newDisplayName)).toBeInTheDocument(); + }); +}); diff --git a/src/course-unit/__mocks__/courseUnitIndex.js b/src/course-unit/__mocks__/courseUnitIndex.js new file mode 100644 index 0000000000..71fb8b1d04 --- /dev/null +++ b/src/course-unit/__mocks__/courseUnitIndex.js @@ -0,0 +1,1123 @@ +module.exports = { + id: 'block-v1:edX+DemoX+Demo_Course+type@vertical+block@867dddb6f55d410caaa9c1eb9c6743ec', + display_name: 'Getting Started', + category: 'vertical', + has_children: true, + edited_on: 'Jan 03, 2024 at 12:06 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, + data: '', + metadata: { + display_name: 'Getting Started', + xml_attributes: { + filename: [ + 'vertical/867dddb6f55d410caaa9c1eb9c6743ec.xml', + 'vertical/867dddb6f55d410caaa9c1eb9c6743ec.xml', + ], + }, + }, + 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 03, 2024 at 12:06 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 03, 2024 at 12:06 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@graded_interactions', + display_name: 'Example Week 1: Getting Started', + category: 'chapter', + has_children: true, + edited_on: 'Jan 03, 2024 at 12:06 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 03, 2024 at 12:06 UTC', + published: true, + published_on: 'Jan 03, 2024 at 08:57 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, +}; diff --git a/src/course-unit/__mocks__/index.js b/src/course-unit/__mocks__/index.js new file mode 100644 index 0000000000..ebf5206845 --- /dev/null +++ b/src/course-unit/__mocks__/index.js @@ -0,0 +1,2 @@ +/* eslint-disable import/prefer-default-export */ +export { default as courseUnitIndexMock } from './courseUnitIndex'; diff --git a/src/course-unit/breadcrumbs/Breadcrumbs.jsx b/src/course-unit/breadcrumbs/Breadcrumbs.jsx new file mode 100644 index 0000000000..ff09ce98ea --- /dev/null +++ b/src/course-unit/breadcrumbs/Breadcrumbs.jsx @@ -0,0 +1,107 @@ +import PropTypes from 'prop-types'; +import { useSelector } from 'react-redux'; +import { useIntl } from '@edx/frontend-platform/i18n'; +import { Dropdown, Icon } from '@edx/paragon'; +import { + ArrowDropDown as ArrowDropDownIcon, + ChevronRight as ChevronRightIcon, +} from '@edx/paragon/icons'; + +import { useCourseOutline } from '../../course-outline/hooks'; +import { getCourseUnitData } from '../data/selectors'; +import messages from './messages'; + +const Breadcrumbs = ({ courseId }) => { + const intl = useIntl(); + const { ancestorInfo } = useSelector(getCourseUnitData); + const { sectionsList, isLoading: isLoadingCourseOutline } = useCourseOutline({ courseId }); + const activeCourseSectionInfo = sectionsList.find((block) => block.id === ancestorInfo?.ancestors[1]?.id); + + const breadcrumbs = { + section: { + id: ancestorInfo?.ancestors[1]?.id, + displayName: ancestorInfo?.ancestors[1]?.displayName, + dropdownItems: sectionsList, + }, + subsection: { + id: ancestorInfo?.ancestors[0]?.id, + displayName: ancestorInfo?.ancestors[0]?.displayName, + dropdownItems: activeCourseSectionInfo?.childInfo.children || [], + }, + }; + + const getLoadingPlaceholder = () => ( +
+ {intl.formatMessage(messages.loading)} +
+ ); + + return ( + + ); +}; + +Breadcrumbs.propTypes = { + courseId: PropTypes.string.isRequired, +}; + +export default Breadcrumbs; diff --git a/src/course-unit/breadcrumbs/Breadcrumbs.scss b/src/course-unit/breadcrumbs/Breadcrumbs.scss new file mode 100644 index 0000000000..6dcf731348 --- /dev/null +++ b/src/course-unit/breadcrumbs/Breadcrumbs.scss @@ -0,0 +1,11 @@ +.course-unit { + .sub-header-title .sub-header-breadcrumbs { + .dropdown-toggle::after { + display: none; + } + + [aria-expanded="true"] .pgn__icon { + transform: scale(1, -1); + } + } +} diff --git a/src/course-unit/breadcrumbs/Breadcrumbs.test.jsx b/src/course-unit/breadcrumbs/Breadcrumbs.test.jsx new file mode 100644 index 0000000000..3c001ca470 --- /dev/null +++ b/src/course-unit/breadcrumbs/Breadcrumbs.test.jsx @@ -0,0 +1,106 @@ +import MockAdapter from 'axios-mock-adapter'; +import { render, waitFor } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import { IntlProvider } from '@edx/frontend-platform/i18n'; +import { initializeMockApp } from '@edx/frontend-platform'; +import { AppProvider } from '@edx/frontend-platform/react'; +import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth'; + +import { getCourseOutlineIndexApiUrl } from '../../course-outline/data/api'; +import { fetchCourseOutlineIndexQuery } from '../../course-outline/data/thunk'; +import { courseOutlineIndexMock } from '../../course-outline/__mocks__'; +import initializeStore from '../../store'; +import { executeThunk } from '../../utils'; +import { getCourseUnitApiUrl } from '../data/api'; +import { fetchCourseUnitQuery } from '../data/thunk'; +import { courseUnitIndexMock } from '../__mocks__'; +import Breadcrumbs from './Breadcrumbs'; + +import messages from './messages'; + +let axiosMock; +let store; +const courseId = '123'; +const breadcrumbsExpected = { + section: { + id: 'block-v1:edX+DemoX+Demo_Course+type@chapter+block@interactive_demonstrations', + displayName: 'Example Week 1: Getting Started', + }, + subsection: { + displayName: 'Lesson 1 - Getting Started', + }, +}; + +const renderComponent = () => render( + + + + + , +); + +describe('', () => { + beforeEach(async () => { + initializeMockApp({ + authenticatedUser: { + userId: 3, + username: 'abc123', + administrator: true, + roles: [], + }, + }); + + store = initializeStore(); + axiosMock = new MockAdapter(getAuthenticatedHttpClient()); + axiosMock + .onGet(getCourseUnitApiUrl(courseId)) + .reply(200, courseUnitIndexMock); + await executeThunk(fetchCourseUnitQuery(courseId), store.dispatch); + }); + + it('render Breadcrumbs component correctly', async () => { + const { getByText } = renderComponent(); + + await waitFor(() => { + expect(getByText(breadcrumbsExpected.section.displayName)).toBeInTheDocument(); + expect(getByText(breadcrumbsExpected.subsection.displayName)).toBeInTheDocument(); + }); + }); + + it('render dropdown loading placeholder on pending', async () => { + const { getByText, queryAllByTestId } = renderComponent(); + + await waitFor(() => { + expect(getByText(breadcrumbsExpected.section.displayName)).toBeInTheDocument(); + expect(getByText(breadcrumbsExpected.subsection.displayName)).toBeInTheDocument(); + }); + + const button = getByText(breadcrumbsExpected.section.displayName); + userEvent.click(button); + + expect(queryAllByTestId('breadcrumbs-section-dropdown-item')).toHaveLength(0); + expect(getByText(messages.loading.defaultMessage)).toBeInTheDocument(); + }); + + it('render Breadcrumbs\'s dropdown menus correctly', async () => { + axiosMock + .onGet(getCourseOutlineIndexApiUrl(courseId)) + .reply(200, courseOutlineIndexMock); + await executeThunk(fetchCourseOutlineIndexQuery(courseId), store.dispatch); + const { getByText, queryAllByTestId } = renderComponent(); + + expect(getByText(breadcrumbsExpected.section.displayName)).toBeInTheDocument(); + expect(getByText(breadcrumbsExpected.subsection.displayName)).toBeInTheDocument(); + expect(queryAllByTestId('breadcrumbs-section-dropdown-item')).toHaveLength(0); + expect(queryAllByTestId('breadcrumbs-subsection-dropdown-item')).toHaveLength(0); + + const button = getByText(breadcrumbsExpected.section.displayName); + userEvent.click(button); + await waitFor(() => { + expect(queryAllByTestId('breadcrumbs-section-dropdown-item')).toHaveLength(4); + }); + + userEvent.click(getByText(breadcrumbsExpected.subsection.displayName)); + expect(queryAllByTestId('breadcrumbs-subsection-dropdown-item')).toHaveLength(3); + }); +}); diff --git a/src/course-unit/breadcrumbs/messages.js b/src/course-unit/breadcrumbs/messages.js new file mode 100644 index 0000000000..9a602ee29b --- /dev/null +++ b/src/course-unit/breadcrumbs/messages.js @@ -0,0 +1,15 @@ +import { defineMessages } from '@edx/frontend-platform/i18n'; + +const messages = defineMessages({ + altIconChevron: { + id: 'course-authoring.course-unit.heading.icon.chevron.alt', + defaultMessage: 'Toggle dropdown menu', + }, + loading: { + id: 'course-authoring.course-unit.heading.breadcrumbs.loading', + defaultMessage: 'Loading...', + description: 'Message displayed while loading breadcrumbs content', + }, +}); + +export default messages; diff --git a/src/course-unit/data/api.js b/src/course-unit/data/api.js new file mode 100644 index 0000000000..feded71eae --- /dev/null +++ b/src/course-unit/data/api.js @@ -0,0 +1,38 @@ +// @ts-check +import { camelCaseObject, getConfig } from '@edx/frontend-platform'; +import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth'; + +const getApiBaseUrl = () => getConfig().STUDIO_BASE_URL; + +export const getCourseUnitApiUrl = (itemId) => `${getApiBaseUrl()}/xblock/container/${itemId}`; + +export const getXBlockBaseApiUrl = (itemId) => `${getApiBaseUrl()}/xblock/${itemId}`; + +/** + * Get course unit. + * @param {string} unitId + * @returns {Promise} + */ +export async function getCourseUnitData(unitId) { + const { data } = await getAuthenticatedHttpClient() + .get(getCourseUnitApiUrl(unitId)); + + return camelCaseObject(data); +} + +/** + * Edit course unit display name. + * @param {string} unitId + * @param {string} displayName + * @returns {Promise} + */ +export async function editUnitDisplayName(unitId, displayName) { + const { data } = await getAuthenticatedHttpClient() + .post(getXBlockBaseApiUrl(unitId), { + metadata: { + display_name: displayName, + }, + }); + + return data; +} diff --git a/src/course-unit/data/selectors.js b/src/course-unit/data/selectors.js new file mode 100644 index 0000000000..c6c449d175 --- /dev/null +++ b/src/course-unit/data/selectors.js @@ -0,0 +1,5 @@ +export const getCourseUnitData = (state) => state.courseUnit.unit; + +export const getSavingStatus = (state) => state.courseUnit.savingStatus; + +export const getLoadingStatus = (state) => state.courseUnit.loadingStatus; diff --git a/src/course-unit/data/slice.js b/src/course-unit/data/slice.js new file mode 100644 index 0000000000..6763fbd0b6 --- /dev/null +++ b/src/course-unit/data/slice.js @@ -0,0 +1,39 @@ +/* eslint-disable no-param-reassign */ +import { createSlice } from '@reduxjs/toolkit'; + +import { RequestStatus } from '../../data/constants'; + +const slice = createSlice({ + name: 'courseUnit', + initialState: { + savingStatus: '', + loadingStatus: { + fetchUnitLoadingStatus: RequestStatus.IN_PROGRESS, + }, + unit: {}, + }, + reducers: { + fetchCourseItemSuccess: (state, { payload }) => { + state.unit = payload; + }, + updateLoadingCourseUnitStatus: (state, { payload }) => { + state.loadingStatus = { + ...state.loadingStatus, + fetchUnitLoadingStatus: payload.status, + }; + }, + updateSavingStatus: (state, { payload }) => { + state.savingStatus = payload.status; + }, + }, +}); + +export const { + fetchCourseItemSuccess, + updateLoadingCourseUnitStatus, + updateSavingStatus, +} = slice.actions; + +export const { + reducer, +} = slice; diff --git a/src/course-unit/data/thunk.js b/src/course-unit/data/thunk.js new file mode 100644 index 0000000000..0c684e88d0 --- /dev/null +++ b/src/course-unit/data/thunk.js @@ -0,0 +1,49 @@ +import { + hideProcessingNotification, + showProcessingNotification, +} from '../../generic/processing-notification/data/slice'; +import { RequestStatus } from '../../data/constants'; +import { NOTIFICATION_MESSAGES } from '../../constants'; +import { getCourseUnitData, editUnitDisplayName } from './api'; +import { + updateLoadingCourseUnitStatus, + fetchCourseItemSuccess, + updateSavingStatus, +} from './slice'; + +export function fetchCourseUnitQuery(courseId) { + return async (dispatch) => { + dispatch(updateLoadingCourseUnitStatus({ status: RequestStatus.IN_PROGRESS })); + + try { + const courseUnit = await getCourseUnitData(courseId); + dispatch(fetchCourseItemSuccess(courseUnit)); + dispatch(updateLoadingCourseUnitStatus({ status: RequestStatus.SUCCESSFUL })); + return true; + } catch (error) { + dispatch(updateLoadingCourseUnitStatus({ status: RequestStatus.FAILED })); + return false; + } + }; +} + +export function editCourseItemQuery(itemId, displayName) { + return async (dispatch) => { + dispatch(updateSavingStatus({ status: RequestStatus.PENDING })); + dispatch(showProcessingNotification(NOTIFICATION_MESSAGES.saving)); + + try { + await editUnitDisplayName(itemId, displayName).then(async (result) => { + if (result) { + const courseUnit = await getCourseUnitData(itemId); + dispatch(fetchCourseItemSuccess(courseUnit)); + dispatch(hideProcessingNotification()); + dispatch(updateSavingStatus({ status: RequestStatus.SUCCESSFUL })); + } + }); + } catch (error) { + dispatch(hideProcessingNotification()); + dispatch(updateSavingStatus({ status: RequestStatus.FAILED })); + } + }; +} diff --git a/src/course-unit/header-navigations/HeaderNavigations.jsx b/src/course-unit/header-navigations/HeaderNavigations.jsx new file mode 100644 index 0000000000..0116dd47ed --- /dev/null +++ b/src/course-unit/header-navigations/HeaderNavigations.jsx @@ -0,0 +1,36 @@ +import PropTypes from 'prop-types'; +import { useIntl } from '@edx/frontend-platform/i18n'; +import { Button } from '@edx/paragon'; + +import messages from './messages'; + +const HeaderNavigations = ({ headerNavigationsActions }) => { + const intl = useIntl(); + const { handleViewLive, handlePreview } = headerNavigationsActions; + + return ( + + ); +}; + +HeaderNavigations.propTypes = { + headerNavigationsActions: PropTypes.shape({ + handleViewLive: PropTypes.func.isRequired, + handlePreview: PropTypes.func.isRequired, + }).isRequired, +}; + +export default HeaderNavigations; diff --git a/src/course-unit/header-navigations/HeaderNavigations.test.jsx b/src/course-unit/header-navigations/HeaderNavigations.test.jsx new file mode 100644 index 0000000000..e5a094247e --- /dev/null +++ b/src/course-unit/header-navigations/HeaderNavigations.test.jsx @@ -0,0 +1,42 @@ +import { fireEvent, render } from '@testing-library/react'; +import { IntlProvider } from '@edx/frontend-platform/i18n'; + +import HeaderNavigations from './HeaderNavigations'; +import messages from './messages'; + +const handleViewLiveFn = jest.fn(); +const handlePreviewFn = jest.fn(); +const headerNavigationsActions = { + handleViewLive: handleViewLiveFn, + handlePreview: handlePreviewFn, +}; + +const renderComponent = (props) => render( + + + , +); + +describe('', () => { + it('render HeaderNavigations component correctly', () => { + const { getByRole } = renderComponent(); + + expect(getByRole('button', { name: messages.viewLiveButton.defaultMessage })).toBeInTheDocument(); + expect(getByRole('button', { name: messages.previewButton.defaultMessage })).toBeInTheDocument(); + }); + + it('calls the correct handlers when clicking buttons', () => { + const { getByRole } = renderComponent(); + + const viewLiveButton = getByRole('button', { name: messages.viewLiveButton.defaultMessage }); + fireEvent.click(viewLiveButton); + expect(handleViewLiveFn).toHaveBeenCalledTimes(1); + + const previewButton = getByRole('button', { name: messages.previewButton.defaultMessage }); + fireEvent.click(previewButton); + expect(handlePreviewFn).toHaveBeenCalledTimes(1); + }); +}); diff --git a/src/course-unit/header-navigations/messages.js b/src/course-unit/header-navigations/messages.js new file mode 100644 index 0000000000..55e60fc965 --- /dev/null +++ b/src/course-unit/header-navigations/messages.js @@ -0,0 +1,14 @@ +import { defineMessages } from '@edx/frontend-platform/i18n'; + +const messages = defineMessages({ + viewLiveButton: { + id: 'course-authoring.course-unit.button.view-live', + defaultMessage: 'View live version', + }, + previewButton: { + id: 'course-authoring.course-unit.button.preview', + defaultMessage: 'Preview', + }, +}); + +export default messages; diff --git a/src/course-unit/header-title/HeaderTitle.jsx b/src/course-unit/header-title/HeaderTitle.jsx new file mode 100644 index 0000000000..bf5e09d9c2 --- /dev/null +++ b/src/course-unit/header-title/HeaderTitle.jsx @@ -0,0 +1,67 @@ +import { useEffect, useState } from 'react'; +import PropTypes from 'prop-types'; +import { useIntl } from '@edx/frontend-platform/i18n'; +import { Form, IconButton } from '@edx/paragon'; +import { + EditOutline as EditIcon, + Settings as SettingsIcon, +} from '@edx/paragon/icons'; + +import messages from './messages'; + +const HeaderTitle = ({ + unitTitle, + isTitleEditFormOpen, + handleTitleEdit, + handleTitleEditSubmit, +}) => { + const intl = useIntl(); + const [titleValue, setTitleValue] = useState(unitTitle); + + useEffect(() => { + setTitleValue(unitTitle); + }, [unitTitle]); + + return ( +
+ {isTitleEditFormOpen ? ( + + e && e.focus()} + value={titleValue} + name="displayName" + onChange={(e) => setTitleValue(e.target.value)} + aria-label={intl.formatMessage(messages.ariaLabelButtonEdit)} + onBlur={() => handleTitleEditSubmit(titleValue)} + onKeyDown={(e) => { + if (e.key === 'Enter') { + handleTitleEditSubmit(titleValue); + } + }} + /> + + ) : unitTitle} + + {}} + /> +
+ ); +}; + +HeaderTitle.propTypes = { + unitTitle: PropTypes.string.isRequired, + isTitleEditFormOpen: PropTypes.bool.isRequired, + handleTitleEdit: PropTypes.func.isRequired, + handleTitleEditSubmit: PropTypes.func.isRequired, +}; + +export default HeaderTitle; diff --git a/src/course-unit/header-title/HeaderTitle.test.jsx b/src/course-unit/header-title/HeaderTitle.test.jsx new file mode 100644 index 0000000000..855a28ea85 --- /dev/null +++ b/src/course-unit/header-title/HeaderTitle.test.jsx @@ -0,0 +1,69 @@ +import { render } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import { IntlProvider } from '@edx/frontend-platform/i18n'; + +import HeaderTitle from './HeaderTitle'; +import messages from './messages'; + +const unitTitle = 'Getting Started'; +const isTitleEditFormOpen = false; +const handleTitleEdit = jest.fn(); +const handleTitleEditSubmit = jest.fn(); + +const renderComponent = (props) => render( + + + , +); + +describe('', () => { + it('render HeaderTitle component correctly', () => { + const { getByText, getByRole } = renderComponent(); + + expect(getByText(unitTitle)).toBeInTheDocument(); + expect(getByRole('button', { name: messages.altButtonEdit.defaultMessage })).toBeInTheDocument(); + expect(getByRole('button', { name: messages.altButtonSettings.defaultMessage })).toBeInTheDocument(); + }); + + it('render HeaderTitle with open edit form', () => { + const { getByRole } = renderComponent({ + isTitleEditFormOpen: true, + }); + + expect(getByRole('textbox', { name: messages.ariaLabelButtonEdit.defaultMessage })).toBeInTheDocument(); + expect(getByRole('textbox', { name: messages.ariaLabelButtonEdit.defaultMessage })).toHaveValue(unitTitle); + expect(getByRole('button', { name: messages.altButtonEdit.defaultMessage })).toBeInTheDocument(); + expect(getByRole('button', { name: messages.altButtonSettings.defaultMessage })).toBeInTheDocument(); + }); + + it('calls toggle edit title form by clicking on Edit button', () => { + const { getByRole } = renderComponent(); + + const editTitleButton = getByRole('button', { name: messages.altButtonEdit.defaultMessage }); + userEvent.click(editTitleButton); + expect(handleTitleEdit).toHaveBeenCalledTimes(1); + }); + + it('calls saving title by clicking outside or press Enter key', async () => { + const { getByRole } = renderComponent({ + isTitleEditFormOpen: true, + }); + + const titleField = getByRole('textbox', { name: messages.ariaLabelButtonEdit.defaultMessage }); + userEvent.type(titleField, ' 1'); + expect(titleField).toHaveValue(`${unitTitle} 1`); + userEvent.click(document.body); + expect(handleTitleEditSubmit).toHaveBeenCalledTimes(1); + + userEvent.click(titleField); + userEvent.type(titleField, ' 2[Enter]'); + expect(titleField).toHaveValue(`${unitTitle} 1 2`); + expect(handleTitleEditSubmit).toHaveBeenCalledTimes(2); + }); +}); diff --git a/src/course-unit/header-title/messages.js b/src/course-unit/header-title/messages.js new file mode 100644 index 0000000000..c6ca9ef208 --- /dev/null +++ b/src/course-unit/header-title/messages.js @@ -0,0 +1,18 @@ +import { defineMessages } from '@edx/frontend-platform/i18n'; + +const messages = defineMessages({ + altButtonEdit: { + id: 'course-authoring.course-unit.heading.button.edit.alt', + defaultMessage: 'Edit', + }, + ariaLabelButtonEdit: { + id: 'course-authoring.course-unit.heading.button.edit.aria-label', + defaultMessage: 'Edit field', + }, + altButtonSettings: { + id: 'course-authoring.course-unit.heading.button.settings.alt', + defaultMessage: 'Settings', + }, +}); + +export default messages; diff --git a/src/course-unit/hooks.jsx b/src/course-unit/hooks.jsx new file mode 100644 index 0000000000..98e4fc9a84 --- /dev/null +++ b/src/course-unit/hooks.jsx @@ -0,0 +1,75 @@ +import { useContext, useEffect, useState } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; +import { AppContext } from '@edx/frontend-platform/react'; + +import { RequestStatus } from '../data/constants'; +import { + fetchCourseUnitQuery, + editCourseItemQuery, +} from './data/thunk'; +import { + getCourseUnitData, + getLoadingStatus, + getSavingStatus, +} from './data/selectors'; +import { updateSavingStatus } from './data/slice'; +import { getUnitViewLivePath, getUnitPreviewPath } from './utils'; + +const useCourseUnit = ({ courseId, blockId }) => { + const dispatch = useDispatch(); + + const { config } = useContext(AppContext); + const courseUnit = useSelector(getCourseUnitData); + const savingStatus = useSelector(getSavingStatus); + const loadingStatus = useSelector(getLoadingStatus); + + const [isTitleEditFormOpen, toggleTitleEditForm] = useState(false); + + const unitTitle = courseUnit.metadata?.displayName || ''; + + const headerNavigationsActions = { + handleViewLive: () => { + window.open(config.LMS_BASE_URL + getUnitViewLivePath(courseId, blockId), '_blank'); + }, + handlePreview: () => { + const subsectionId = courseUnit.ancestorInfo?.ancestors[0]?.id.split('@').pop(); + const sectionId = courseUnit.ancestorInfo?.ancestors[1]?.id.split('@').pop(); + window.open(config.PREVIEW_BASE_URL + getUnitPreviewPath(courseId, sectionId, subsectionId, blockId), '_blank'); + }, + }; + + const handleInternetConnectionFailed = () => { + dispatch(updateSavingStatus({ status: RequestStatus.FAILED })); + }; + + const handleTitleEdit = () => { + toggleTitleEditForm(!isTitleEditFormOpen); + }; + + const handleTitleEditSubmit = (displayName) => { + if (unitTitle !== displayName) { + dispatch(editCourseItemQuery(blockId, displayName)); + } + + handleTitleEdit(); + }; + + useEffect(() => { + dispatch(fetchCourseUnitQuery(blockId)); + }, [courseId]); + + return { + courseUnit, + unitTitle, + isLoading: loadingStatus.fetchUnitLoadingStatus === RequestStatus.IN_PROGRESS, + isTitleEditFormOpen, + isInternetConnectionAlertFailed: savingStatus === RequestStatus.FAILED, + handleInternetConnectionFailed, + headerNavigationsActions, + handleTitleEdit, + handleTitleEditSubmit, + }; +}; + +// eslint-disable-next-line import/prefer-default-export +export { useCourseUnit }; diff --git a/src/course-unit/index.js b/src/course-unit/index.js new file mode 100644 index 0000000000..e6c38e561a --- /dev/null +++ b/src/course-unit/index.js @@ -0,0 +1,2 @@ +/* eslint-disable import/prefer-default-export */ +export { default as CourseUnit } from './CourseUnit'; diff --git a/src/course-unit/messages.js b/src/course-unit/messages.js new file mode 100644 index 0000000000..42533513e5 --- /dev/null +++ b/src/course-unit/messages.js @@ -0,0 +1,10 @@ +import { defineMessages } from '@edx/frontend-platform/i18n'; + +const messages = defineMessages({ + alertFailedGeneric: { + id: 'course-authoring.course-unit.general.alert.error.description', + defaultMessage: 'Unable to {actionName} {type}. Please try again.', + }, +}); + +export default messages; diff --git a/src/course-unit/utils.jsx b/src/course-unit/utils.jsx new file mode 100644 index 0000000000..5bd8b7a376 --- /dev/null +++ b/src/course-unit/utils.jsx @@ -0,0 +1,23 @@ +/* eslint-disable max-len */ +// @ts-check + +/** + * Method to return course unit view live URL path. + * @param {string} courseId + * @param {string} blockId + * @returns {string} {`/courses/${string}/jump_to/${string}`} + */ +export const getUnitViewLivePath = (courseId, blockId) => ( + `/courses/${courseId}/jump_to/${blockId}` +); + +/** + * Method to return course unit preview URL path. + * @param {string} courseId + * @param {string} sectionId + * @param {string} blockId + * @returns {string} {`/courses/${courseId}/courseware/interactive_demonstrations/${sectionId}/1?activate_block_id=${blockId}`} + */ +export const getUnitPreviewPath = (courseId, sectionId, subsectionId, blockId) => ( + `/courses/${courseId}/courseware/${sectionId}/${subsectionId}/1?activate_block_id=${blockId}` +); diff --git a/src/generic/sub-header/SubHeader.jsx b/src/generic/sub-header/SubHeader.jsx index 53030ff863..463afcd86d 100644 --- a/src/generic/sub-header/SubHeader.jsx +++ b/src/generic/sub-header/SubHeader.jsx @@ -5,6 +5,7 @@ import { ActionRow } from '@edx/paragon'; const SubHeader = ({ title, subtitle, + breadcrumbs, contentTitle, description, instruction, @@ -17,6 +18,9 @@ const SubHeader = ({

{subtitle} + {breadcrumbs && ( +
{breadcrumbs}
+ )} {title} {titleActions && ( @@ -25,7 +29,7 @@ const SubHeader = ({ )}

{headerActions && ( - + {headerActions} )} @@ -41,19 +45,29 @@ const SubHeader = ({ )} ); + SubHeader.defaultProps = { instruction: '', description: '', subtitle: '', + breadcrumbs: '', contentTitle: '', headerActions: null, titleActions: null, hideBorder: false, withSubHeaderContent: true, }; + SubHeader.propTypes = { - title: PropTypes.string.isRequired, + title: PropTypes.oneOfType([ + PropTypes.node, + PropTypes.string, + ]).isRequired, subtitle: PropTypes.string, + breadcrumbs: PropTypes.oneOfType([ + PropTypes.node, + PropTypes.string, + ]), contentTitle: PropTypes.string, description: PropTypes.string, instruction: PropTypes.oneOfType([ diff --git a/src/generic/sub-header/SubHeader.scss b/src/generic/sub-header/SubHeader.scss index 693d5603b9..dc76222220 100644 --- a/src/generic/sub-header/SubHeader.scss +++ b/src/generic/sub-header/SubHeader.scss @@ -1,5 +1,6 @@ .sub-header { display: flex; + gap: map-get($spacers, 4\.5); .sub-header-actions { margin-bottom: 1.75rem; @@ -11,7 +12,8 @@ font: normal $font-weight-bold 2rem/2.25rem $font-family-base; color: $black; - .sub-header-title-subtitle { + .sub-header-title-subtitle, + .sub-header-breadcrumbs { font: normal $font-weight-normal .875rem/1.5rem $font-family-base; display: block; color: $text-color-base; diff --git a/src/i18n/messages/ar.json b/src/i18n/messages/ar.json index 79c292c0f3..c4d728a394 100644 --- a/src/i18n/messages/ar.json +++ b/src/i18n/messages/ar.json @@ -975,5 +975,12 @@ "course-authoring.studio-home.verify-email.banner.title": "We need to verify your email address", "course-authoring.studio-home.verify-email.banner.description": "Almost there! In order to complete your sign up we need you to verify your email address ({email}). An activation message and next steps should be waiting for you there.", "course-authoring.studio-home.verify-email.sidebar.title": "Need help?", - "course-authoring.studio-home.verify-email.sidebar.description": "Please check your Junk or Spam folders in case our email isn't in your INBOX. Still can't find the verification email? Request help via the link below." + "course-authoring.studio-home.verify-email.sidebar.description": "Please check your Junk or Spam folders in case our email isn't in your INBOX. Still can't find the verification email? Request help via the link below.", + "course-authoring.course-unit.heading.icon.chevron.alt": "Toggle dropdown menu", + "course-authoring.course-unit.button.view-live": "View live version", + "course-authoring.course-unit.button.preview": "Preview", + "course-authoring.course-unit.heading.button.edit.alt": "Edit", + "course-authoring.course-unit.heading.button.edit.aria-label": "Edit field", + "course-authoring.course-unit.heading.button.settings.alt": "Settings", + "course-authoring.course-unit.general.alert.error.description": "Unable to {actionName} {type}. Please try again." } diff --git a/src/i18n/messages/de.json b/src/i18n/messages/de.json index 526e1f518e..4c8578fcf8 100644 --- a/src/i18n/messages/de.json +++ b/src/i18n/messages/de.json @@ -975,5 +975,12 @@ "course-authoring.studio-home.verify-email.banner.title": "We need to verify your email address", "course-authoring.studio-home.verify-email.banner.description": "Almost there! In order to complete your sign up we need you to verify your email address ({email}). An activation message and next steps should be waiting for you there.", "course-authoring.studio-home.verify-email.sidebar.title": "Need help?", - "course-authoring.studio-home.verify-email.sidebar.description": "Please check your Junk or Spam folders in case our email isn't in your INBOX. Still can't find the verification email? Request help via the link below." + "course-authoring.studio-home.verify-email.sidebar.description": "Please check your Junk or Spam folders in case our email isn't in your INBOX. Still can't find the verification email? Request help via the link below.", + "course-authoring.course-unit.heading.icon.chevron.alt": "Toggle dropdown menu", + "course-authoring.course-unit.button.view-live": "View live version", + "course-authoring.course-unit.button.preview": "Preview", + "course-authoring.course-unit.heading.button.edit.alt": "Edit", + "course-authoring.course-unit.heading.button.edit.aria-label": "Edit field", + "course-authoring.course-unit.heading.button.settings.alt": "Settings", + "course-authoring.course-unit.general.alert.error.description": "Unable to {actionName} {type}. Please try again." } diff --git a/src/i18n/messages/de_DE.json b/src/i18n/messages/de_DE.json index ed2621dfe9..d69cba4406 100644 --- a/src/i18n/messages/de_DE.json +++ b/src/i18n/messages/de_DE.json @@ -975,5 +975,12 @@ "course-authoring.studio-home.verify-email.banner.title": "We need to verify your email address", "course-authoring.studio-home.verify-email.banner.description": "Almost there! In order to complete your sign up we need you to verify your email address ({email}). An activation message and next steps should be waiting for you there.", "course-authoring.studio-home.verify-email.sidebar.title": "Need help?", - "course-authoring.studio-home.verify-email.sidebar.description": "Please check your Junk or Spam folders in case our email isn't in your INBOX. Still can't find the verification email? Request help via the link below." + "course-authoring.studio-home.verify-email.sidebar.description": "Please check your Junk or Spam folders in case our email isn't in your INBOX. Still can't find the verification email? Request help via the link below.", + "course-authoring.course-unit.heading.icon.chevron.alt": "Toggle dropdown menu", + "course-authoring.course-unit.button.view-live": "View live version", + "course-authoring.course-unit.button.preview": "Preview", + "course-authoring.course-unit.heading.button.edit.alt": "Edit", + "course-authoring.course-unit.heading.button.edit.aria-label": "Edit field", + "course-authoring.course-unit.heading.button.settings.alt": "Settings", + "course-authoring.course-unit.general.alert.error.description": "Unable to {actionName} {type}. Please try again." } diff --git a/src/i18n/messages/es_419.json b/src/i18n/messages/es_419.json index 2312d83390..1111d4cae1 100644 --- a/src/i18n/messages/es_419.json +++ b/src/i18n/messages/es_419.json @@ -975,5 +975,12 @@ "course-authoring.studio-home.verify-email.banner.title": "Necesitamos verificar su dirección de correo electrónico", "course-authoring.studio-home.verify-email.banner.description": "¡Ya casi terminamos! Para completar su registro, necesitamos que verifique su dirección de correo electrónico ({email}). Un mensaje de activación y los pasos a seguir le estarán esperando allí.", "course-authoring.studio-home.verify-email.sidebar.title": "¿Necesita ayuda?", - "course-authoring.studio-home.verify-email.sidebar.description": "Por favor revise su correo no desado en caso de que nuestro correo no esté en su buzón de entrada. ¿Aún no encuentra el correo de verificación? Pida ayuda a través del vínculo siguiente." + "course-authoring.studio-home.verify-email.sidebar.description": "Por favor revise su correo no desado en caso de que nuestro correo no esté en su buzón de entrada. ¿Aún no encuentra el correo de verificación? Pida ayuda a través del vínculo siguiente.", + "course-authoring.course-unit.heading.icon.chevron.alt": "Toggle dropdown menu", + "course-authoring.course-unit.button.view-live": "View live version", + "course-authoring.course-unit.button.preview": "Preview", + "course-authoring.course-unit.heading.button.edit.alt": "Edit", + "course-authoring.course-unit.heading.button.edit.aria-label": "Edit field", + "course-authoring.course-unit.heading.button.settings.alt": "Settings", + "course-authoring.course-unit.general.alert.error.description": "Unable to {actionName} {type}. Please try again." } diff --git a/src/i18n/messages/fa_IR.json b/src/i18n/messages/fa_IR.json index 9e26dfeeb6..3c58659757 100644 --- a/src/i18n/messages/fa_IR.json +++ b/src/i18n/messages/fa_IR.json @@ -1 +1,9 @@ -{} \ No newline at end of file +{ + "course-authoring.course-unit.heading.icon.chevron.alt": "Toggle dropdown menu", + "course-authoring.course-unit.button.view-live": "View live version", + "course-authoring.course-unit.button.preview": "Preview", + "course-authoring.course-unit.heading.button.edit.alt": "Edit", + "course-authoring.course-unit.heading.button.edit.aria-label": "Edit field", + "course-authoring.course-unit.heading.button.settings.alt": "Settings", + "course-authoring.course-unit.general.alert.error.description": "Unable to {actionName} {type}. Please try again." +} diff --git a/src/i18n/messages/fr.json b/src/i18n/messages/fr.json index 786d42f27a..094d6af15b 100644 --- a/src/i18n/messages/fr.json +++ b/src/i18n/messages/fr.json @@ -975,5 +975,12 @@ "course-authoring.studio-home.verify-email.banner.title": "We need to verify your email address", "course-authoring.studio-home.verify-email.banner.description": "Almost there! In order to complete your sign up we need you to verify your email address ({email}). An activation message and next steps should be waiting for you there.", "course-authoring.studio-home.verify-email.sidebar.title": "Need help?", - "course-authoring.studio-home.verify-email.sidebar.description": "Please check your Junk or Spam folders in case our email isn't in your INBOX. Still can't find the verification email? Request help via the link below." + "course-authoring.studio-home.verify-email.sidebar.description": "Please check your Junk or Spam folders in case our email isn't in your INBOX. Still can't find the verification email? Request help via the link below.", + "course-authoring.course-unit.heading.icon.chevron.alt": "Toggle dropdown menu", + "course-authoring.course-unit.button.view-live": "View live version", + "course-authoring.course-unit.button.preview": "Preview", + "course-authoring.course-unit.heading.button.edit.alt": "Edit", + "course-authoring.course-unit.heading.button.edit.aria-label": "Edit field", + "course-authoring.course-unit.heading.button.settings.alt": "Settings", + "course-authoring.course-unit.general.alert.error.description": "Unable to {actionName} {type}. Please try again." } diff --git a/src/i18n/messages/fr_CA.json b/src/i18n/messages/fr_CA.json index e3e4b7a3ad..8ed7611d8a 100644 --- a/src/i18n/messages/fr_CA.json +++ b/src/i18n/messages/fr_CA.json @@ -975,5 +975,12 @@ "course-authoring.studio-home.verify-email.banner.title": "Nous devons vérifier votre adresse courriel", "course-authoring.studio-home.verify-email.banner.description": "Presque là! Afin de finaliser votre inscription, nous avons besoin que vous vérifiiez votre adresse courriel ({email}). Un message d’activation et les prochaines étapes devraient vous y attendre.", "course-authoring.studio-home.verify-email.sidebar.title": "Besoin d'aide?", - "course-authoring.studio-home.verify-email.sidebar.description": "Merci de vérifier votre corbeille ou votre dossier de pourriel au cas où notre courriel ne se trouve pas dans votre boite de réception. Vous ne trouvez toujours pas le courriel de vérification? Demandez de l'aide via le lien ci-dessous." + "course-authoring.studio-home.verify-email.sidebar.description": "Merci de vérifier votre corbeille ou votre dossier de pourriel au cas où notre courriel ne se trouve pas dans votre boite de réception. Vous ne trouvez toujours pas le courriel de vérification? Demandez de l'aide via le lien ci-dessous.", + "course-authoring.course-unit.heading.icon.chevron.alt": "Toggle dropdown menu", + "course-authoring.course-unit.button.view-live": "View live version", + "course-authoring.course-unit.button.preview": "Preview", + "course-authoring.course-unit.heading.button.edit.alt": "Edit", + "course-authoring.course-unit.heading.button.edit.aria-label": "Edit field", + "course-authoring.course-unit.heading.button.settings.alt": "Settings", + "course-authoring.course-unit.general.alert.error.description": "Unable to {actionName} {type}. Please try again." } diff --git a/src/i18n/messages/hi.json b/src/i18n/messages/hi.json index 526e1f518e..4c8578fcf8 100644 --- a/src/i18n/messages/hi.json +++ b/src/i18n/messages/hi.json @@ -975,5 +975,12 @@ "course-authoring.studio-home.verify-email.banner.title": "We need to verify your email address", "course-authoring.studio-home.verify-email.banner.description": "Almost there! In order to complete your sign up we need you to verify your email address ({email}). An activation message and next steps should be waiting for you there.", "course-authoring.studio-home.verify-email.sidebar.title": "Need help?", - "course-authoring.studio-home.verify-email.sidebar.description": "Please check your Junk or Spam folders in case our email isn't in your INBOX. Still can't find the verification email? Request help via the link below." + "course-authoring.studio-home.verify-email.sidebar.description": "Please check your Junk or Spam folders in case our email isn't in your INBOX. Still can't find the verification email? Request help via the link below.", + "course-authoring.course-unit.heading.icon.chevron.alt": "Toggle dropdown menu", + "course-authoring.course-unit.button.view-live": "View live version", + "course-authoring.course-unit.button.preview": "Preview", + "course-authoring.course-unit.heading.button.edit.alt": "Edit", + "course-authoring.course-unit.heading.button.edit.aria-label": "Edit field", + "course-authoring.course-unit.heading.button.settings.alt": "Settings", + "course-authoring.course-unit.general.alert.error.description": "Unable to {actionName} {type}. Please try again." } diff --git a/src/i18n/messages/it.json b/src/i18n/messages/it.json index 526e1f518e..4c8578fcf8 100644 --- a/src/i18n/messages/it.json +++ b/src/i18n/messages/it.json @@ -975,5 +975,12 @@ "course-authoring.studio-home.verify-email.banner.title": "We need to verify your email address", "course-authoring.studio-home.verify-email.banner.description": "Almost there! In order to complete your sign up we need you to verify your email address ({email}). An activation message and next steps should be waiting for you there.", "course-authoring.studio-home.verify-email.sidebar.title": "Need help?", - "course-authoring.studio-home.verify-email.sidebar.description": "Please check your Junk or Spam folders in case our email isn't in your INBOX. Still can't find the verification email? Request help via the link below." + "course-authoring.studio-home.verify-email.sidebar.description": "Please check your Junk or Spam folders in case our email isn't in your INBOX. Still can't find the verification email? Request help via the link below.", + "course-authoring.course-unit.heading.icon.chevron.alt": "Toggle dropdown menu", + "course-authoring.course-unit.button.view-live": "View live version", + "course-authoring.course-unit.button.preview": "Preview", + "course-authoring.course-unit.heading.button.edit.alt": "Edit", + "course-authoring.course-unit.heading.button.edit.aria-label": "Edit field", + "course-authoring.course-unit.heading.button.settings.alt": "Settings", + "course-authoring.course-unit.general.alert.error.description": "Unable to {actionName} {type}. Please try again." } diff --git a/src/i18n/messages/it_IT.json b/src/i18n/messages/it_IT.json index 0072e6110d..46998e7cc8 100644 --- a/src/i18n/messages/it_IT.json +++ b/src/i18n/messages/it_IT.json @@ -975,5 +975,12 @@ "course-authoring.studio-home.verify-email.banner.title": "We need to verify your email address", "course-authoring.studio-home.verify-email.banner.description": "Almost there! In order to complete your sign up we need you to verify your email address ({email}). An activation message and next steps should be waiting for you there.", "course-authoring.studio-home.verify-email.sidebar.title": "Need help?", - "course-authoring.studio-home.verify-email.sidebar.description": "Please check your Junk or Spam folders in case our email isn't in your INBOX. Still can't find the verification email? Request help via the link below." + "course-authoring.studio-home.verify-email.sidebar.description": "Please check your Junk or Spam folders in case our email isn't in your INBOX. Still can't find the verification email? Request help via the link below.", + "course-authoring.course-unit.heading.icon.chevron.alt": "Toggle dropdown menu", + "course-authoring.course-unit.button.view-live": "View live version", + "course-authoring.course-unit.button.preview": "Preview", + "course-authoring.course-unit.heading.button.edit.alt": "Edit", + "course-authoring.course-unit.heading.button.edit.aria-label": "Edit field", + "course-authoring.course-unit.heading.button.settings.alt": "Settings", + "course-authoring.course-unit.general.alert.error.description": "Unable to {actionName} {type}. Please try again." } diff --git a/src/i18n/messages/pt.json b/src/i18n/messages/pt.json index 526e1f518e..4c8578fcf8 100644 --- a/src/i18n/messages/pt.json +++ b/src/i18n/messages/pt.json @@ -975,5 +975,12 @@ "course-authoring.studio-home.verify-email.banner.title": "We need to verify your email address", "course-authoring.studio-home.verify-email.banner.description": "Almost there! In order to complete your sign up we need you to verify your email address ({email}). An activation message and next steps should be waiting for you there.", "course-authoring.studio-home.verify-email.sidebar.title": "Need help?", - "course-authoring.studio-home.verify-email.sidebar.description": "Please check your Junk or Spam folders in case our email isn't in your INBOX. Still can't find the verification email? Request help via the link below." + "course-authoring.studio-home.verify-email.sidebar.description": "Please check your Junk or Spam folders in case our email isn't in your INBOX. Still can't find the verification email? Request help via the link below.", + "course-authoring.course-unit.heading.icon.chevron.alt": "Toggle dropdown menu", + "course-authoring.course-unit.button.view-live": "View live version", + "course-authoring.course-unit.button.preview": "Preview", + "course-authoring.course-unit.heading.button.edit.alt": "Edit", + "course-authoring.course-unit.heading.button.edit.aria-label": "Edit field", + "course-authoring.course-unit.heading.button.settings.alt": "Settings", + "course-authoring.course-unit.general.alert.error.description": "Unable to {actionName} {type}. Please try again." } diff --git a/src/i18n/messages/pt_PT.json b/src/i18n/messages/pt_PT.json index 97514b1a97..52eea7d8e2 100644 --- a/src/i18n/messages/pt_PT.json +++ b/src/i18n/messages/pt_PT.json @@ -975,5 +975,12 @@ "course-authoring.studio-home.verify-email.banner.title": "We need to verify your email address", "course-authoring.studio-home.verify-email.banner.description": "Almost there! In order to complete your sign up we need you to verify your email address ({email}). An activation message and next steps should be waiting for you there.", "course-authoring.studio-home.verify-email.sidebar.title": "Need help?", - "course-authoring.studio-home.verify-email.sidebar.description": "Please check your Junk or Spam folders in case our email isn't in your INBOX. Still can't find the verification email? Request help via the link below." + "course-authoring.studio-home.verify-email.sidebar.description": "Please check your Junk or Spam folders in case our email isn't in your INBOX. Still can't find the verification email? Request help via the link below.", + "course-authoring.course-unit.heading.icon.chevron.alt": "Toggle dropdown menu", + "course-authoring.course-unit.button.view-live": "View live version", + "course-authoring.course-unit.button.preview": "Preview", + "course-authoring.course-unit.heading.button.edit.alt": "Edit", + "course-authoring.course-unit.heading.button.edit.aria-label": "Edit field", + "course-authoring.course-unit.heading.button.settings.alt": "Settings", + "course-authoring.course-unit.general.alert.error.description": "Unable to {actionName} {type}. Please try again." } diff --git a/src/i18n/messages/ru.json b/src/i18n/messages/ru.json index 526e1f518e..4c8578fcf8 100644 --- a/src/i18n/messages/ru.json +++ b/src/i18n/messages/ru.json @@ -975,5 +975,12 @@ "course-authoring.studio-home.verify-email.banner.title": "We need to verify your email address", "course-authoring.studio-home.verify-email.banner.description": "Almost there! In order to complete your sign up we need you to verify your email address ({email}). An activation message and next steps should be waiting for you there.", "course-authoring.studio-home.verify-email.sidebar.title": "Need help?", - "course-authoring.studio-home.verify-email.sidebar.description": "Please check your Junk or Spam folders in case our email isn't in your INBOX. Still can't find the verification email? Request help via the link below." + "course-authoring.studio-home.verify-email.sidebar.description": "Please check your Junk or Spam folders in case our email isn't in your INBOX. Still can't find the verification email? Request help via the link below.", + "course-authoring.course-unit.heading.icon.chevron.alt": "Toggle dropdown menu", + "course-authoring.course-unit.button.view-live": "View live version", + "course-authoring.course-unit.button.preview": "Preview", + "course-authoring.course-unit.heading.button.edit.alt": "Edit", + "course-authoring.course-unit.heading.button.edit.aria-label": "Edit field", + "course-authoring.course-unit.heading.button.settings.alt": "Settings", + "course-authoring.course-unit.general.alert.error.description": "Unable to {actionName} {type}. Please try again." } diff --git a/src/i18n/messages/uk.json b/src/i18n/messages/uk.json index 526e1f518e..4c8578fcf8 100644 --- a/src/i18n/messages/uk.json +++ b/src/i18n/messages/uk.json @@ -975,5 +975,12 @@ "course-authoring.studio-home.verify-email.banner.title": "We need to verify your email address", "course-authoring.studio-home.verify-email.banner.description": "Almost there! In order to complete your sign up we need you to verify your email address ({email}). An activation message and next steps should be waiting for you there.", "course-authoring.studio-home.verify-email.sidebar.title": "Need help?", - "course-authoring.studio-home.verify-email.sidebar.description": "Please check your Junk or Spam folders in case our email isn't in your INBOX. Still can't find the verification email? Request help via the link below." + "course-authoring.studio-home.verify-email.sidebar.description": "Please check your Junk or Spam folders in case our email isn't in your INBOX. Still can't find the verification email? Request help via the link below.", + "course-authoring.course-unit.heading.icon.chevron.alt": "Toggle dropdown menu", + "course-authoring.course-unit.button.view-live": "View live version", + "course-authoring.course-unit.button.preview": "Preview", + "course-authoring.course-unit.heading.button.edit.alt": "Edit", + "course-authoring.course-unit.heading.button.edit.aria-label": "Edit field", + "course-authoring.course-unit.heading.button.settings.alt": "Settings", + "course-authoring.course-unit.general.alert.error.description": "Unable to {actionName} {type}. Please try again." } diff --git a/src/i18n/messages/zh_CN.json b/src/i18n/messages/zh_CN.json index 526e1f518e..4c8578fcf8 100644 --- a/src/i18n/messages/zh_CN.json +++ b/src/i18n/messages/zh_CN.json @@ -975,5 +975,12 @@ "course-authoring.studio-home.verify-email.banner.title": "We need to verify your email address", "course-authoring.studio-home.verify-email.banner.description": "Almost there! In order to complete your sign up we need you to verify your email address ({email}). An activation message and next steps should be waiting for you there.", "course-authoring.studio-home.verify-email.sidebar.title": "Need help?", - "course-authoring.studio-home.verify-email.sidebar.description": "Please check your Junk or Spam folders in case our email isn't in your INBOX. Still can't find the verification email? Request help via the link below." + "course-authoring.studio-home.verify-email.sidebar.description": "Please check your Junk or Spam folders in case our email isn't in your INBOX. Still can't find the verification email? Request help via the link below.", + "course-authoring.course-unit.heading.icon.chevron.alt": "Toggle dropdown menu", + "course-authoring.course-unit.button.view-live": "View live version", + "course-authoring.course-unit.button.preview": "Preview", + "course-authoring.course-unit.heading.button.edit.alt": "Edit", + "course-authoring.course-unit.heading.button.edit.aria-label": "Edit field", + "course-authoring.course-unit.heading.button.settings.alt": "Settings", + "course-authoring.course-unit.general.alert.error.description": "Unable to {actionName} {type}. Please try again." } diff --git a/src/index.jsx b/src/index.jsx index 3488cca8cd..962c5d228c 100755 --- a/src/index.jsx +++ b/src/index.jsx @@ -104,6 +104,7 @@ initialize({ SUPPORT_URL: process.env.SUPPORT_URL || null, SUPPORT_EMAIL: process.env.SUPPORT_EMAIL || null, LEARNING_BASE_URL: process.env.LEARNING_BASE_URL, + PREVIEW_BASE_URL: process.env.PREVIEW_BASE_URL, EXAMS_BASE_URL: process.env.EXAMS_BASE_URL || null, CALCULATOR_HELP_URL: process.env.CALCULATOR_HELP_URL || null, ENABLE_PROGRESS_GRAPH_SETTINGS: process.env.ENABLE_PROGRESS_GRAPH_SETTINGS || 'false', diff --git a/src/setupTest.js b/src/setupTest.js index b4feef6c1f..ef139dcbfb 100755 --- a/src/setupTest.js +++ b/src/setupTest.js @@ -46,6 +46,7 @@ mergeConfig({ CALCULATOR_HELP_URL: process.env.CALCULATOR_HELP_URL || null, ENABLE_PROGRESS_GRAPH_SETTINGS: process.env.ENABLE_PROGRESS_GRAPH_SETTINGS || 'false', ENABLE_TEAM_TYPE_SETTING: process.env.ENABLE_TEAM_TYPE_SETTING === 'true', + PREVIEW_BASE_URL: process.env.PREVIEW_BASE_URL || '', }, 'CourseAuthoringConfig'); // Mock the plugins repo so jest will stop complaining about ES6 syntax diff --git a/src/store.js b/src/store.js index c20e405193..ee193470b8 100644 --- a/src/store.js +++ b/src/store.js @@ -20,6 +20,7 @@ import { reducer as genericReducer } from './generic/data/slice'; import { reducer as courseImportReducer } from './import-page/data/slice'; import { reducer as videosReducer } from './files-and-videos/videos-page/data/slice'; import { reducer as courseOutlineReducer } from './course-outline/data/slice'; +import { reducer as courseUnitReducer } from './course-unit/data/slice'; export default function initializeStore(preloadedState = undefined) { return configureStore({ @@ -44,6 +45,7 @@ export default function initializeStore(preloadedState = undefined) { courseImport: courseImportReducer, videos: videosReducer, courseOutline: courseOutlineReducer, + courseUnit: courseUnitReducer, }, preloadedState, });