diff --git a/src/constants.js b/src/constants.js index 163a16ef84..9e5c5cc84d 100644 --- a/src/constants.js +++ b/src/constants.js @@ -58,6 +58,7 @@ export const COURSE_BLOCK_NAMES = ({ chapter: { id: 'chapter', name: 'Section' }, sequential: { id: 'sequential', name: 'Subsection' }, vertical: { id: 'vertical', name: 'Unit' }, + libraryContent: { id: 'library_content', name: 'Library content' }, component: { id: 'component', name: 'Component' }, }); diff --git a/src/course-unit/CourseUnit.jsx b/src/course-unit/CourseUnit.jsx index 4c54d6775e..a8aa7471a6 100644 --- a/src/course-unit/CourseUnit.jsx +++ b/src/course-unit/CourseUnit.jsx @@ -22,6 +22,7 @@ import ProcessingNotification from '../generic/processing-notification'; import { SavingErrorAlert } from '../generic/saving-error-alert'; import ConnectionErrorAlert from '../generic/ConnectionErrorAlert'; import Loading from '../generic/Loading'; +import { COURSE_BLOCK_NAMES } from '../constants'; import AddComponent from './add-component/AddComponent'; import HeaderTitle from './header-title/HeaderTitle'; import Breadcrumbs from './breadcrumbs/Breadcrumbs'; @@ -45,6 +46,7 @@ const CourseUnit = ({ courseId }) => { isLoading, sequenceId, unitTitle, + unitCategory, errorMessage, sequenceStatus, savingStatus, @@ -71,6 +73,19 @@ const CourseUnit = ({ courseId }) => { handleNavigateToTargetUnit, } = useCourseUnit({ courseId, blockId }); + const isUnitVerticalType = unitCategory === COURSE_BLOCK_NAMES.vertical.id; + const isUnitLibraryType = unitCategory === COURSE_BLOCK_NAMES.libraryContent.id; + + const unitLayout = [{ span: 12 }, { span: 0 }]; + const defaultLayout = { + lg: [{ span: 8 }, { span: 4 }], + md: [{ span: 8 }, { span: 4 }], + sm: [{ span: 8 }, { span: 3 }], + xs: [{ span: 9 }, { span: 3 }], + xl: [{ span: 9 }, { span: 3 }], + }; + const layoutGrid = isUnitLibraryType ? { lg: unitLayout } : defaultLayout; + useEffect(() => { document.title = getPageHeadTitle('', unitTitle); }, [unitTitle]); @@ -142,28 +157,28 @@ const CourseUnit = ({ courseId }) => { /> )} breadcrumbs={( - + )} headerActions={( )} /> - - + {isUnitVerticalType && ( + + )} + {currentlyVisibleToStudents && ( { unitXBlockActions={unitXBlockActions} courseVerticalChildren={courseVerticalChildren.children} /> - - {showPasteXBlock && canPasteComponent && ( + {isUnitVerticalType && ( + + )} + {showPasteXBlock && canPasteComponent && isUnitVerticalType && ( { - - - - {getConfig().ENABLE_TAGGING_TAXONOMY_PAGES === 'true' - && ( - - - + {isUnitVerticalType && ( + <> + + + + {getConfig().ENABLE_TAGGING_TAXONOMY_PAGES === 'true' && ( + + + + )} + + + + )} - - - diff --git a/src/course-unit/CourseUnit.scss b/src/course-unit/CourseUnit.scss index a2d6124ba3..abc649b986 100644 --- a/src/course-unit/CourseUnit.scss +++ b/src/course-unit/CourseUnit.scss @@ -6,6 +6,10 @@ @import "./move-modal"; @import "./preview-changes"; +.course-unit { + min-width: 900px; +} + .course-unit__alert { margin-bottom: 1.75rem; } diff --git a/src/course-unit/CourseUnit.test.jsx b/src/course-unit/CourseUnit.test.jsx index 63fb3bf1d1..fbfbd68112 100644 --- a/src/course-unit/CourseUnit.test.jsx +++ b/src/course-unit/CourseUnit.test.jsx @@ -52,6 +52,7 @@ import sidebarMessages from './sidebar/messages'; import { extractCourseUnitId } from './sidebar/utils'; import CourseUnit from './CourseUnit'; +import { getClipboardUrl } from '../generic/data/api'; import configureModalMessages from '../generic/configure-modal/messages'; import { getContentTaxonomyTagsApiUrl, getContentTaxonomyTagsCountApiUrl } from '../content-tags-drawer/data/api'; import addComponentMessages from './add-component/messages'; @@ -138,6 +139,9 @@ describe('', () => { global.localStorage.clear(); store = initializeStore(); axiosMock = new MockAdapter(getAuthenticatedHttpClient()); + axiosMock + .onGet(getClipboardUrl()) + .reply(200, clipboardUnit); axiosMock .onGet(getCourseUnitApiUrl(courseId)) .reply(200, courseUnitIndexMock); @@ -226,6 +230,19 @@ describe('', () => { display_name: newDisplayName, }, }); + axiosMock + .onGet(getCourseSectionVerticalApiUrl(blockId)) + .reply(200, { + ...courseSectionVerticalMock, + xblock_info: { + ...courseSectionVerticalMock.xblock_info, + display_name: newDisplayName, + }, + xblock: { + ...courseSectionVerticalMock.xblock, + display_name: newDisplayName, + }, + }); await waitFor(() => { const unitHeaderTitle = getByTestId('unit-header-title'); @@ -914,9 +931,7 @@ describe('', () => { .reply(200, clipboardMockResponse); axiosMock .onGet(getCourseSectionVerticalApiUrl(blockId)) - .reply(200, { - ...updatedCourseSectionVerticalData, - }); + .reply(200, updatedCourseSectionVerticalData); global.localStorage.setItem('staticFileNotices', JSON.stringify(clipboardMockResponse.staticFileNotices)); await executeThunk(fetchCourseSectionVerticalData(blockId), store.dispatch); @@ -1190,7 +1205,7 @@ describe('', () => { axiosMock .onGet(getCourseUnitApiUrl(blockId)) - .reply(200, {}); + .reply(200, courseUnitIndexMock); await act(async () => { await waitFor(() => { diff --git a/src/course-unit/add-component/AddComponent.jsx b/src/course-unit/add-component/AddComponent.jsx index 70962a7ac6..598b82b59f 100644 --- a/src/course-unit/add-component/AddComponent.jsx +++ b/src/course-unit/add-component/AddComponent.jsx @@ -23,7 +23,7 @@ const AddComponent = ({ blockId, handleCreateNewCourseXBlock }) => { const [isOpenAdvanced, openAdvanced, closeAdvanced] = useToggle(false); const [isOpenHtml, openHtml, closeHtml] = useToggle(false); const [isOpenOpenAssessment, openOpenAssessment, closeOpenAssessment] = useToggle(false); - const { componentTemplates } = useSelector(getCourseSectionVertical); + const { componentTemplates = {} } = useSelector(getCourseSectionVertical); const [isAddLibraryContentModalOpen, showAddLibraryContentModal, closeAddLibraryContentModal] = useToggle(); const [isSelectLibraryContentModalOpen, showSelectLibraryContentModal, closeSelectLibraryContentModal] = useToggle(); const [selectedComponents, setSelectedComponents] = useState([]); diff --git a/src/course-unit/add-component/add-component-btn/AddComponentIcon.jsx b/src/course-unit/add-component/add-component-btn/AddComponentIcon.jsx index 91cc5b09b1..030e50b2ea 100644 --- a/src/course-unit/add-component/add-component-btn/AddComponentIcon.jsx +++ b/src/course-unit/add-component/add-component-btn/AddComponentIcon.jsx @@ -2,7 +2,7 @@ import PropTypes from 'prop-types'; import { Icon } from '@openedx/paragon'; import { EditNote as EditNoteIcon } from '@openedx/paragon/icons'; -import { COMPONENT_TYPES, COMPONENT_TYPE_ICON_MAP } from '../../../generic/block-type-utils/constants'; +import { COMPONENT_TYPE_ICON_MAP } from '../../../generic/block-type-utils/constants'; const AddComponentIcon = ({ type }) => { const icon = COMPONENT_TYPE_ICON_MAP[type] || EditNoteIcon; @@ -11,7 +11,7 @@ const AddComponentIcon = ({ type }) => { }; AddComponentIcon.propTypes = { - type: PropTypes.oneOf(Object.values(COMPONENT_TYPES)).isRequired, + type: PropTypes.string.isRequired, }; export default AddComponentIcon; diff --git a/src/course-unit/add-component/add-component-btn/index.jsx b/src/course-unit/add-component/add-component-btn/index.jsx index 8745806d6e..cbeb54fe26 100644 --- a/src/course-unit/add-component/add-component-btn/index.jsx +++ b/src/course-unit/add-component/add-component-btn/index.jsx @@ -2,6 +2,7 @@ import PropTypes from 'prop-types'; import { Badge, Button } from '@openedx/paragon'; import { useIntl } from '@edx/frontend-platform/i18n'; +import { COMPONENT_TYPES } from '../../../generic/block-type-utils/constants'; import messages from '../messages'; import AddComponentIcon from './AddComponentIcon'; diff --git a/src/course-unit/add-component/add-component-modals/ComponentModalView.jsx b/src/course-unit/add-component/add-component-modals/ComponentModalView.jsx index 84fbc16115..dcbd9e45c3 100644 --- a/src/course-unit/add-component/add-component-modals/ComponentModalView.jsx +++ b/src/course-unit/add-component/add-component-modals/ComponentModalView.jsx @@ -72,7 +72,7 @@ const ComponentModalView = ({ + {supportLabels[componentTemplate.supportLevel].tooltip} )} diff --git a/src/course-unit/breadcrumbs/Breadcrumbs.jsx b/src/course-unit/breadcrumbs/Breadcrumbs.jsx index 26bfa53562..23db4e7a56 100644 --- a/src/course-unit/breadcrumbs/Breadcrumbs.jsx +++ b/src/course-unit/breadcrumbs/Breadcrumbs.jsx @@ -1,3 +1,4 @@ +import PropTypes from 'prop-types'; import { useSelector } from 'react-redux'; import { useIntl } from '@edx/frontend-platform/i18n'; import { Dropdown, Icon } from '@openedx/paragon'; @@ -10,77 +11,79 @@ import { getConfig } from '@edx/frontend-platform'; import { getWaffleFlags } from '../../data/selectors'; import { getCourseSectionVertical } from '../data/selectors'; +import { adoptCourseSectionUrl } from '../utils'; import messages from './messages'; -const Breadcrumbs = () => { +const Breadcrumbs = ({ courseId, sequenceId }) => { const intl = useIntl(); - const { ancestorXblocks } = useSelector(getCourseSectionVertical); - const [section, subsection] = ancestorXblocks ?? []; + const { ancestorXblocks = [] } = useSelector(getCourseSectionVertical); const waffleFlags = useSelector(getWaffleFlags); const getPathToCourseOutlinePage = (url) => (waffleFlags.useNewCourseOutlinePage ? url : `${getConfig().STUDIO_BASE_URL}${url}`); + const getPathToCourseUnitPage = (url) => (waffleFlags.useNewUnitPage + ? adoptCourseSectionUrl({ url, courseId, sequenceId }) + : `${getConfig().STUDIO_BASE_URL}${url}`); + + const getPathToCoursePage = (isOutlinePage, url) => ( + isOutlinePage ? getPathToCourseOutlinePage(url) : getPathToCourseUnitPage(url) + ); + return ( ); }; +Breadcrumbs.propTypes = { + courseId: PropTypes.string.isRequired, + sequenceId: PropTypes.string.isRequired, +}; + export default Breadcrumbs; diff --git a/src/course-unit/breadcrumbs/Breadcrumbs.test.jsx b/src/course-unit/breadcrumbs/Breadcrumbs.test.jsx index d20a35c339..b0e9a71fbc 100644 --- a/src/course-unit/breadcrumbs/Breadcrumbs.test.jsx +++ b/src/course-unit/breadcrumbs/Breadcrumbs.test.jsx @@ -15,6 +15,7 @@ import Breadcrumbs from './Breadcrumbs'; let axiosMock; let reduxStore; const courseId = '123'; +const sequenceId = '456'; const mockNavigate = jest.fn(); const breadcrumbsExpected = { section: { @@ -32,7 +33,7 @@ jest.mock('react-router-dom', () => ({ })); const renderComponent = () => render( - , + , ); describe('', () => { @@ -80,11 +81,13 @@ describe('', () => { const button = getByText(breadcrumbsExpected.section.displayName); userEvent.click(button); await waitFor(() => { - expect(queryAllByTestId('breadcrumbs-section-dropdown-item')).toHaveLength(5); + expect(queryAllByTestId('breadcrumbs-dropdown-item-level0')).toHaveLength(5); }); userEvent.click(getByText(breadcrumbsExpected.subsection.displayName)); - expect(queryAllByTestId('breadcrumbs-subsection-dropdown-item')).toHaveLength(2); + await waitFor(() => { + expect(queryAllByTestId('breadcrumbs-dropdown-item-level1')).toHaveLength(2); + }); }); it('navigates using the new course outline page when the waffle flag is enabled', async () => { diff --git a/src/course-unit/clipboard/paste-notification/components/FileList.jsx b/src/course-unit/clipboard/paste-notification/components/FileList.jsx index f3f9e3beaa..148b622539 100644 --- a/src/course-unit/clipboard/paste-notification/components/FileList.jsx +++ b/src/course-unit/clipboard/paste-notification/components/FileList.jsx @@ -5,7 +5,7 @@ import { FILE_LIST_DEFAULT_VALUE } from '../constants'; const FileList = ({ fileList }) => (
    {fileList.map((fileName) => ( -
  • {fileName}
  • +
  • {fileName}
  • ))}
); diff --git a/src/course-unit/clipboard/paste-notification/index.jsx b/src/course-unit/clipboard/paste-notification/index.jsx index b92334c717..20eed888df 100644 --- a/src/course-unit/clipboard/paste-notification/index.jsx +++ b/src/course-unit/clipboard/paste-notification/index.jsx @@ -101,7 +101,7 @@ const PastNotificationAlert = ({ staticFileNotices, courseId }) => { PastNotificationAlert.propTypes = { courseId: PropTypes.string.isRequired, staticFileNotices: - PropTypes.objectOf({ + PropTypes.shape({ conflictingFiles: PropTypes.arrayOf(PropTypes.string), errorFiles: PropTypes.arrayOf(PropTypes.string), newFiles: PropTypes.arrayOf(PropTypes.string), diff --git a/src/course-unit/constants.js b/src/course-unit/constants.js index 129fa55d9b..aaf5e728aa 100644 --- a/src/course-unit/constants.js +++ b/src/course-unit/constants.js @@ -55,6 +55,7 @@ export const messageTypes = { showMultipleComponentPicker: 'showMultipleComponentPicker', addSelectedComponentsToBank: 'addSelectedComponentsToBank', showXBlockLibraryChangesPreview: 'showXBlockLibraryChangesPreview', + handleViewXBlockContent: 'handleViewXBlockContent', }; export const IFRAME_FEATURE_POLICY = ( diff --git a/src/course-unit/context/iFrameContext.tsx b/src/course-unit/context/iFrameContext.tsx index 75418f0d39..ab216bb79a 100644 --- a/src/course-unit/context/iFrameContext.tsx +++ b/src/course-unit/context/iFrameContext.tsx @@ -1,4 +1,4 @@ -import { +import React, { createContext, MutableRefObject, useRef, useCallback, useMemo, ReactNode, } from 'react'; import { logError } from '@edx/frontend-platform/logging'; diff --git a/src/course-unit/data/api.js b/src/course-unit/data/api.js index 039285dcf4..7a46974060 100644 --- a/src/course-unit/data/api.js +++ b/src/course-unit/data/api.js @@ -99,6 +99,7 @@ export async function createCourseXblock({ * @param {string} type - The action type (e.g., PUBLISH_TYPES.discardChanges). * @param {boolean} isVisible - The visibility status for students. * @param {boolean} groupAccess - Access group key set. + * @param {boolean} isDiscussionEnabled - Indicates whether the discussion feature is enabled. * @returns {Promise} A promise that resolves with the response data. */ export async function handleCourseUnitVisibilityAndData(unitId, type, isVisible, groupAccess, isDiscussionEnabled) { diff --git a/src/course-unit/data/thunk.js b/src/course-unit/data/thunk.js index a0d421eea3..99dac4b690 100644 --- a/src/course-unit/data/thunk.js +++ b/src/course-unit/data/thunk.js @@ -70,11 +70,11 @@ export function fetchCourseSectionVerticalData(courseId, sequenceId) { dispatch(updateLoadingCourseSectionVerticalDataStatus({ status: RequestStatus.SUCCESSFUL })); dispatch(updateModel({ modelType: 'sequences', - model: courseSectionVerticalData.sequence, + model: courseSectionVerticalData.sequence || [], })); dispatch(updateModels({ modelType: 'units', - models: courseSectionVerticalData.units, + models: courseSectionVerticalData.units || [], })); dispatch(fetchStaticFileNoticesSuccess(JSON.parse(localStorage.getItem('staticFileNotices')))); localStorage.removeItem('staticFileNotices'); @@ -103,11 +103,11 @@ export function editCourseItemQuery(itemId, displayName, sequenceId) { dispatch(updateLoadingCourseSectionVerticalDataStatus({ status: RequestStatus.SUCCESSFUL })); dispatch(updateModel({ modelType: 'sequences', - model: courseSectionVerticalData.sequence, + model: courseSectionVerticalData.sequence || [], })); dispatch(updateModels({ modelType: 'units', - models: courseSectionVerticalData.units, + models: courseSectionVerticalData.units || [], })); dispatch(fetchSequenceSuccess({ sequenceId })); dispatch(fetchCourseItemSuccess(courseUnit)); diff --git a/src/course-unit/data/utils.js b/src/course-unit/data/utils.js index def2b38492..0b28805297 100644 --- a/src/course-unit/data/utils.js +++ b/src/course-unit/data/utils.js @@ -10,9 +10,9 @@ export function normalizeCourseSectionVerticalData(metadata) { sequence: { id: data.subsectionLocation, title: data.xblock.displayName, - unitIds: data.xblockInfo.ancestorInfo.ancestors[0].childInfo.children.map((item) => item.id), + unitIds: data.xblockInfo.ancestorInfo?.ancestors[0].childInfo.children.map((item) => item.id), }, - units: data.xblockInfo.ancestorInfo.ancestors[0].childInfo.children.map((unit) => ({ + units: data.xblockInfo.ancestorInfo?.ancestors[0].childInfo.children.map((unit) => ({ id: unit.id, sequenceId: data.subsectionLocation, bookmarked: unit.bookmarked, diff --git a/src/course-unit/header-navigations/HeaderNavigations.jsx b/src/course-unit/header-navigations/HeaderNavigations.jsx index 178c768dfd..a934c0c974 100644 --- a/src/course-unit/header-navigations/HeaderNavigations.jsx +++ b/src/course-unit/header-navigations/HeaderNavigations.jsx @@ -1,27 +1,42 @@ import PropTypes from 'prop-types'; import { useIntl } from '@edx/frontend-platform/i18n'; import { Button } from '@openedx/paragon'; +import { Edit as EditIcon } from '@openedx/paragon/icons'; +import { COURSE_BLOCK_NAMES } from '../../constants'; import messages from './messages'; -const HeaderNavigations = ({ headerNavigationsActions }) => { +const HeaderNavigations = ({ headerNavigationsActions, unitCategory }) => { const intl = useIntl(); - const { handleViewLive, handlePreview } = headerNavigationsActions; + const { handleViewLive, handlePreview, handleEdit } = headerNavigationsActions; return ( ); }; @@ -30,7 +45,9 @@ HeaderNavigations.propTypes = { headerNavigationsActions: PropTypes.shape({ handleViewLive: PropTypes.func.isRequired, handlePreview: PropTypes.func.isRequired, + handleEdit: PropTypes.func.isRequired, }).isRequired, + unitCategory: PropTypes.string.isRequired, }; export default HeaderNavigations; diff --git a/src/course-unit/header-navigations/HeaderNavigations.test.jsx b/src/course-unit/header-navigations/HeaderNavigations.test.jsx index e5a094247e..1c93905cec 100644 --- a/src/course-unit/header-navigations/HeaderNavigations.test.jsx +++ b/src/course-unit/header-navigations/HeaderNavigations.test.jsx @@ -1,14 +1,18 @@ import { fireEvent, render } from '@testing-library/react'; import { IntlProvider } from '@edx/frontend-platform/i18n'; +import { COURSE_BLOCK_NAMES } from '../../constants'; import HeaderNavigations from './HeaderNavigations'; import messages from './messages'; const handleViewLiveFn = jest.fn(); const handlePreviewFn = jest.fn(); +const handleEditFn = jest.fn(); + const headerNavigationsActions = { handleViewLive: handleViewLiveFn, handlePreview: handlePreviewFn, + handleEdit: handleEditFn, }; const renderComponent = (props) => render( @@ -22,14 +26,14 @@ const renderComponent = (props) => render( describe('', () => { it('render HeaderNavigations component correctly', () => { - const { getByRole } = renderComponent(); + const { getByRole } = renderComponent({ unitCategory: COURSE_BLOCK_NAMES.vertical.id }); 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(); + it('calls the correct handlers when clicking buttons for unit page', () => { + const { getByRole, queryByRole } = renderComponent({ unitCategory: COURSE_BLOCK_NAMES.vertical.id }); const viewLiveButton = getByRole('button', { name: messages.viewLiveButton.defaultMessage }); fireEvent.click(viewLiveButton); @@ -38,5 +42,22 @@ describe('', () => { const previewButton = getByRole('button', { name: messages.previewButton.defaultMessage }); fireEvent.click(previewButton); expect(handlePreviewFn).toHaveBeenCalledTimes(1); + + const editButton = queryByRole('button', { name: messages.editButton.defaultMessage }); + expect(editButton).not.toBeInTheDocument(); + }); + + it('calls the correct handlers when clicking buttons for library page', () => { + const { getByRole, queryByRole } = renderComponent({ unitCategory: COURSE_BLOCK_NAMES.libraryContent.id }); + + const editButton = getByRole('button', { name: messages.editButton.defaultMessage }); + fireEvent.click(editButton); + expect(handleViewLiveFn).toHaveBeenCalledTimes(1); + + const viewLiveButton = queryByRole('button', { name: messages.viewLiveButton.defaultMessage }); + expect(viewLiveButton).not.toBeInTheDocument(); + + const previewButton = queryByRole('button', { name: messages.previewButton.defaultMessage }); + expect(previewButton).not.toBeInTheDocument(); }); }); diff --git a/src/course-unit/header-navigations/messages.js b/src/course-unit/header-navigations/messages.ts similarity index 59% rename from src/course-unit/header-navigations/messages.js rename to src/course-unit/header-navigations/messages.ts index 55e60fc965..1a58965085 100644 --- a/src/course-unit/header-navigations/messages.js +++ b/src/course-unit/header-navigations/messages.ts @@ -4,10 +4,17 @@ const messages = defineMessages({ viewLiveButton: { id: 'course-authoring.course-unit.button.view-live', defaultMessage: 'View live version', + description: 'The unit view live button text', }, previewButton: { id: 'course-authoring.course-unit.button.preview', defaultMessage: 'Preview', + description: 'The unit preview button text', + }, + editButton: { + id: 'course-authoring.course-unit.button.preview', + defaultMessage: 'Edit', + description: 'The unit edit button text', }, }); diff --git a/src/course-unit/header-title/HeaderTitle.jsx b/src/course-unit/header-title/HeaderTitle.jsx index 336d986fab..2219b467e4 100644 --- a/src/course-unit/header-title/HeaderTitle.jsx +++ b/src/course-unit/header-title/HeaderTitle.jsx @@ -9,6 +9,7 @@ import { } from '@openedx/paragon/icons'; import ConfigureModal from '../../generic/configure-modal/ConfigureModal'; +import { COURSE_BLOCK_NAMES } from '../../constants'; import { getCourseUnitData } from '../data/selectors'; import { updateQueryPendingStatus } from '../data/slice'; import messages from './messages'; @@ -86,6 +87,9 @@ const HeaderTitle = ({ onConfigureSubmit={onConfigureSubmit} currentItemData={currentItemData} isSelfPaced={false} + isXBlockComponent={ + [COURSE_BLOCK_NAMES.libraryContent.id, COURSE_BLOCK_NAMES.component.id].includes(currentItemData.category) + } /> {getVisibilityMessage()} diff --git a/src/course-unit/header-title/HeaderTitle.test.jsx b/src/course-unit/header-title/HeaderTitle.test.jsx index 7e57c408e0..6fe98c5582 100644 --- a/src/course-unit/header-title/HeaderTitle.test.jsx +++ b/src/course-unit/header-title/HeaderTitle.test.jsx @@ -108,7 +108,7 @@ describe('', () => { ...courseUnitIndexMock, user_partition_info: { ...courseUnitIndexMock.user_partition_info, - selected_partition_index: '1', + selected_partition_index: 1, selected_groups_label: 'Visibility group 1', }, }); diff --git a/src/course-unit/hooks.jsx b/src/course-unit/hooks.jsx index 11731cc2ad..a5a96095c9 100644 --- a/src/course-unit/hooks.jsx +++ b/src/course-unit/hooks.jsx @@ -1,10 +1,11 @@ -import { useEffect } from 'react'; +import { useCallback, useEffect } from 'react'; import { useDispatch, useSelector } from 'react-redux'; import { useNavigate, useSearchParams } from 'react-router-dom'; import { useToggle } from '@openedx/paragon'; import { RequestStatus } from '../data/constants'; import { useCopyToClipboard } from '../generic/clipboard'; +import { useEventListener } from '../generic/hooks'; import { createNewCourseXBlock, fetchCourseUnitQuery, @@ -49,7 +50,7 @@ export const useCourseUnit = ({ courseId, blockId }) => { const isLoading = useSelector(getIsLoading); const errorMessage = useSelector(getErrorMessage); const sequenceStatus = useSelector(getSequenceStatus); - const { draftPreviewLink, publishedPreviewLink } = useSelector(getCourseSectionVertical); + const { draftPreviewLink, publishedPreviewLink, xblockInfo = {} } = useSelector(getCourseSectionVertical); const courseVerticalChildren = useSelector(getCourseVerticalChildren); const staticFileNotices = useSelector(getStaticFileNotices); const navigate = useNavigate(); @@ -60,8 +61,7 @@ export const useCourseUnit = ({ courseId, blockId }) => { const { currentlyVisibleToStudents } = courseUnit; const { sharedClipboardData, showPasteXBlock, showPasteUnit } = useCopyToClipboard(canEdit); const { canPasteComponent } = courseVerticalChildren; - - const unitTitle = courseUnit.metadata?.displayName || ''; + const { displayName: unitTitle, category: unitCategory } = xblockInfo; const sequenceId = courseUnit.ancestorInfo?.ancestors[0].id; const headerNavigationsActions = { @@ -71,6 +71,7 @@ export const useCourseUnit = ({ courseId, blockId }) => { handlePreview: () => { window.open(draftPreviewLink, '_blank'); }, + handleEdit: () => {}, }; const handleTitleEdit = () => { @@ -86,7 +87,9 @@ export const useCourseUnit = ({ courseId, blockId }) => { isDiscussionEnabled, blockId, )); - closeModalFn(); + if (typeof closeModalFn === 'function') { + closeModalFn(); + } }; const handleTitleEditSubmit = (displayName) => { @@ -150,6 +153,17 @@ export const useCourseUnit = ({ courseId, blockId }) => { navigate(`/course/${courseId}/container/${movedXBlockParams.targetParentLocator}`); }; + const receiveMessage = useCallback(({ data }) => { + const { payload, type } = data; + + if (type === messageTypes.handleViewXBlockContent) { + const newUnitId = payload.destination.split('/').pop(); + navigate(`/course/${courseId}/container/${newUnitId}/${sequenceId}`); + } + }, []); + + useEventListener('message', receiveMessage); + useEffect(() => { if (savingStatus === RequestStatus.SUCCESSFUL) { dispatch(updateQueryPendingStatus(true)); @@ -175,6 +189,7 @@ export const useCourseUnit = ({ courseId, blockId }) => { sequenceId, courseUnit, unitTitle, + unitCategory, errorMessage, sequenceStatus, savingStatus, diff --git a/src/course-unit/move-modal/index.tsx b/src/course-unit/move-modal/index.tsx index 7844d7c310..220e1320f1 100644 --- a/src/course-unit/move-modal/index.tsx +++ b/src/course-unit/move-modal/index.tsx @@ -102,6 +102,7 @@ const MoveModal: FC = ({ onClose={handleCLoseModal} size="xl" className="move-xblock-modal" + title={intl.formatMessage(messages.moveModalTitle, { displayName })} hasCloseButton isFullscreenOnMobile > diff --git a/src/course-unit/move-modal/moveModal.test.tsx b/src/course-unit/move-modal/moveModal.test.tsx index ba94e018a9..6080a8c42e 100644 --- a/src/course-unit/move-modal/moveModal.test.tsx +++ b/src/course-unit/move-modal/moveModal.test.tsx @@ -4,8 +4,8 @@ import { AppProvider } from '@edx/frontend-platform/react'; import { IntlProvider } from '@edx/frontend-platform/i18n'; import { camelCaseObject, initializeMockApp } from '@edx/frontend-platform'; import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth'; -import userEvent from '@testing-library/user-event'; +import userEvent from '@testing-library/user-event'; import initializeStore from '../../store'; import { getCourseOutlineInfoUrl } from '../data/api'; import { courseOutlineInfoMock } from '../__mocks__'; @@ -79,7 +79,9 @@ describe('', () => { const categoryIndicator: HTMLElement = getByTestId('move-xblock-modal-category'); expect(getByText(messages.moveModalTitle.defaultMessage.replace(' {displayName}', ''))).toBeInTheDocument(); - expect(within(breadcrumbs).getByText(messages.moveModalBreadcrumbsBaseCategory.defaultMessage)).toBeInTheDocument(); + expect( + within(breadcrumbs).getByText(messages.moveModalBreadcrumbsBaseCategory.defaultMessage), + ).toBeInTheDocument(); expect( within(categoryIndicator).getByText(messages.moveModalBreadcrumbsSections.defaultMessage), ).toBeInTheDocument(); @@ -95,7 +97,9 @@ describe('', () => { const breadcrumbs: HTMLElement = getByTestId('move-xblock-modal-breadcrumbs'); const categoryIndicator: HTMLElement = getByTestId('move-xblock-modal-category'); - expect(within(breadcrumbs).getByText(messages.moveModalBreadcrumbsBaseCategory.defaultMessage)).toBeInTheDocument(); + expect( + within(breadcrumbs).getByText(messages.moveModalBreadcrumbsBaseCategory.defaultMessage), + ).toBeInTheDocument(); expect( within(categoryIndicator).getByText(messages.moveModalBreadcrumbsSections.defaultMessage), ).toBeInTheDocument(); diff --git a/src/course-unit/sidebar/components/sidebar-footer/index.jsx b/src/course-unit/sidebar/components/sidebar-footer/index.jsx index ee1e816bad..62af6c672b 100644 --- a/src/course-unit/sidebar/components/sidebar-footer/index.jsx +++ b/src/course-unit/sidebar/components/sidebar-footer/index.jsx @@ -43,9 +43,9 @@ const SidebarFooter = ({ SidebarFooter.propTypes = { locationId: PropTypes.string, displayUnitLocation: PropTypes.bool, - openDiscardModal: PropTypes.func.isRequired, - openVisibleModal: PropTypes.func.isRequired, - handlePublishing: PropTypes.func.isRequired, + openDiscardModal: PropTypes.func, + openVisibleModal: PropTypes.func, + handlePublishing: PropTypes.func, visibleToStaffOnly: PropTypes.bool.isRequired, }; diff --git a/src/course-unit/utils.ts b/src/course-unit/utils.ts new file mode 100644 index 0000000000..c26043e12d --- /dev/null +++ b/src/course-unit/utils.ts @@ -0,0 +1,31 @@ +/** + * Adapts API URL paths to the application's internal URL format based on predefined conditions. + * + * @param {Object} params - Parameters for URL adaptation. + * @param {string} params.url - The original API URL to transform. + * @param {string} params.courseId - The course ID. + * @param {string} params.sequenceId - The sequence ID. + * @returns {string} - A correctly formatted internal route for the application. + */ +// eslint-disable-next-line import/prefer-default-export +export const adoptCourseSectionUrl = ( + { url, courseId, sequenceId }: { url: string, courseId: string, sequenceId: string }, +): string => { + let newUrl = url; + const urlConditions = [ + { + regex: /^\/container\/(.+)/, + transform: ([, unitId]) => `/course/${courseId}/container/${unitId}/${sequenceId}`, + }, + ]; + + for (const { regex, transform } of urlConditions) { + const match = RegExp(regex).exec(url); + if (match) { + newUrl = transform([match[0], match[1]]); + break; + } + } + + return newUrl; +}; diff --git a/src/generic/configure-modal/ConfigureModal.jsx b/src/generic/configure-modal/ConfigureModal.jsx index 04c82200df..d57930c4f3 100644 --- a/src/generic/configure-modal/ConfigureModal.jsx +++ b/src/generic/configure-modal/ConfigureModal.jsx @@ -166,6 +166,7 @@ const ConfigureModal = ({ ); break; case COURSE_BLOCK_NAMES.vertical.id: + case COURSE_BLOCK_NAMES.libraryContent.id: case COURSE_BLOCK_NAMES.component.id: // groupAccess should be {partitionId: [group1, group2]} or {} if selectedPartitionIndex === -1 if (data.selectedPartitionIndex >= 0) { @@ -242,10 +243,12 @@ const ConfigureModal = ({ ); case COURSE_BLOCK_NAMES.vertical.id: + case COURSE_BLOCK_NAMES.libraryContent.id: case COURSE_BLOCK_NAMES.component.id: return ( 0 && ( -

+

+ +


@@ -146,10 +149,12 @@ const UnitTab = ({ UnitTab.defaultProps = { isXBlockComponent: false, + isLibraryContent: false, }; UnitTab.propTypes = { isXBlockComponent: PropTypes.bool, + isLibraryContent: PropTypes.bool, values: PropTypes.shape({ isVisibleToStaffOnly: PropTypes.bool.isRequired, discussionEnabled: PropTypes.bool.isRequired, @@ -157,9 +162,7 @@ UnitTab.propTypes = { PropTypes.string, PropTypes.number, ]).isRequired, - selectedGroups: PropTypes.oneOfType([ - PropTypes.string, - ]), + selectedGroups: PropTypes.arrayOf(PropTypes.string), }).isRequired, setFieldValue: PropTypes.func.isRequired, showWarning: PropTypes.bool.isRequired, diff --git a/src/generic/configure-modal/messages.js b/src/generic/configure-modal/messages.js index 41ef703bd8..c7da8ff665 100644 --- a/src/generic/configure-modal/messages.js +++ b/src/generic/configure-modal/messages.js @@ -46,6 +46,10 @@ const messages = defineMessages({ id: 'course-authoring.course-outline.configure-modal.visibility-tab.unit-access', defaultMessage: 'Unit access', }, + libraryContentAccess: { + id: 'course-authoring.course-outline.configure-modal.visibility-tab.unit-access', + defaultMessage: 'Library content access', + }, discussionEnabledSectionTitle: { id: 'course-authoring.course-outline.configure-modal.discussion-enabled.section-title', defaultMessage: 'Discussion', diff --git a/src/setupTest.js b/src/setupTest.js index 4cc847c713..776da0c0b0 100755 --- a/src/setupTest.js +++ b/src/setupTest.js @@ -39,6 +39,10 @@ mergeConfig({ LEARNING_BASE_URL: process.env.LEARNING_BASE_URL, EXAMS_BASE_URL: process.env.EXAMS_BASE_URL || null, CALCULATOR_HELP_URL: process.env.CALCULATOR_HELP_URL || null, + ACCOUNT_PROFILE_URL: process.env.ACCOUNT_PROFILE_URL || null, + ACCOUNT_SETTINGS_URL: process.env.ACCOUNT_SETTINGS_URL || null, + IGNORED_ERROR_REGEX: process.env.IGNORED_ERROR_REGEX || null, + MFE_CONFIG_API_URL: process.env.MFE_CONFIG_API_URL || null, ENABLE_PROGRESS_GRAPH_SETTINGS: process.env.ENABLE_PROGRESS_GRAPH_SETTINGS || 'false', ENABLE_TEAM_TYPE_SETTING: process.env.ENABLE_TEAM_TYPE_SETTING === 'true', ENABLE_CHECKLIST_QUALITY: process.env.ENABLE_CHECKLIST_QUALITY || 'true',