diff --git a/src/course-outline/CourseOutline.jsx b/src/course-outline/CourseOutline.jsx index 63a2c4fdc..2d17118c4 100644 --- a/src/course-outline/CourseOutline.jsx +++ b/src/course-outline/CourseOutline.jsx @@ -19,7 +19,6 @@ import { import { useSelector } from 'react-redux'; import { DraggableList, - SortableItem, ErrorAlert, } from '@edx/frontend-lib-content-components'; @@ -43,6 +42,7 @@ import EmptyPlaceholder from './empty-placeholder/EmptyPlaceholder'; import PublishModal from './publish-modal/PublishModal'; import ConfigureModal from './configure-modal/ConfigureModal'; import DeleteModal from './delete-modal/DeleteModal'; +import ConditionalSortableElement from './drag-helper/ConditionalSortableElement'; import { useCourseOutline } from './hooks'; import messages from './messages'; @@ -53,6 +53,7 @@ const CourseOutline = ({ courseId }) => { courseName, savingStatus, statusBarData, + courseActions, sectionsList, isLoading, isReIndexShow, @@ -175,6 +176,7 @@ const CourseOutline = ({ courseId }) => { headerNavigationsActions={headerNavigationsActions} isDisabledReindexButton={isDisabledReindexButton} hasSections={Boolean(sectionsList.length)} + courseActions={courseActions} /> )} /> @@ -201,9 +203,10 @@ const CourseOutline = ({ courseId }) => { <> {sections.map((section, index) => ( - { updateOrder={finalizeSubsectionOrder(section)} > {section.childInfo.children.map((subsection) => ( - { /> ))} - + ))} - + ))} - + {courseActions.childAddable && ( + + )} ) : ( - + )} diff --git a/src/course-outline/CourseOutline.scss b/src/course-outline/CourseOutline.scss index da2f0321f..3c6572dcf 100644 --- a/src/course-outline/CourseOutline.scss +++ b/src/course-outline/CourseOutline.scss @@ -8,3 +8,4 @@ @import "./highlights-modal/HighlightsModal"; @import "./publish-modal/PublishModal"; @import "./configure-modal/ConfigureModal"; +@import "./drag-helper/ConditionalSortableElement"; diff --git a/src/course-outline/CourseOutline.test.jsx b/src/course-outline/CourseOutline.test.jsx index 93145ccfd..01624b7f7 100644 --- a/src/course-outline/CourseOutline.test.jsx +++ b/src/course-outline/CourseOutline.test.jsx @@ -794,4 +794,36 @@ describe('', () => { expect(subsection1).toBe(subsection1New); }); }); + + it('check that drag handle is not visible for non-draggable sections', async () => { + cleanup(); + axiosMock + .onGet(getCourseOutlineIndexApiUrl(courseId)) + .reply(200, { + ...courseOutlineIndexMock, + courseStructure: { + ...courseOutlineIndexMock.courseStructure, + childInfo: { + ...courseOutlineIndexMock.courseStructure.childInfo, + children: [ + { + ...courseOutlineIndexMock.courseStructure.childInfo.children[0], + actions: { + draggable: false, + childAddable: true, + deletable: true, + duplicable: true, + }, + }, + ...courseOutlineIndexMock.courseStructure.childInfo.children.slice(1), + ], + }, + }, + }); + const { queryByTestId } = render(); + + await waitFor(() => { + expect(queryByTestId('conditional-sortable-element--no-drag-handle')).toBeInTheDocument(); + }); + }); }); diff --git a/src/course-outline/card-header/CardHeader.jsx b/src/course-outline/card-header/CardHeader.jsx index 9c348d503..ee8099efd 100644 --- a/src/course-outline/card-header/CardHeader.jsx +++ b/src/course-outline/card-header/CardHeader.jsx @@ -32,6 +32,7 @@ const CardHeader = ({ onClickDuplicate, titleComponent, namePrefix, + actions, }) => { const intl = useIntl(); const [titleValue, setTitleValue] = useState(title); @@ -103,18 +104,22 @@ const CardHeader = ({ > {intl.formatMessage(messages.menuConfigure)} - - {intl.formatMessage(messages.menuDuplicate)} - - - {intl.formatMessage(messages.menuDelete)} - + {actions.duplicable && ( + + {intl.formatMessage(messages.menuDuplicate)} + + )} + {actions.deletable && ( + + {intl.formatMessage(messages.menuDelete)} + + )} @@ -138,6 +143,12 @@ CardHeader.propTypes = { onClickDuplicate: PropTypes.func.isRequired, titleComponent: PropTypes.node.isRequired, namePrefix: PropTypes.string.isRequired, + actions: PropTypes.shape({ + deletable: PropTypes.bool.isRequired, + draggable: PropTypes.bool.isRequired, + childAddable: PropTypes.bool.isRequired, + duplicable: PropTypes.bool.isRequired, + }).isRequired, }; export default CardHeader; diff --git a/src/course-outline/card-header/CardHeader.test.jsx b/src/course-outline/card-header/CardHeader.test.jsx index f185a3c40..f007034a1 100644 --- a/src/course-outline/card-header/CardHeader.test.jsx +++ b/src/course-outline/card-header/CardHeader.test.jsx @@ -30,6 +30,12 @@ const cardHeaderProps = { onClickDelete: onClickDeleteMock, onClickDuplicate: onClickDuplicateMock, namePrefix: 'section', + actions: { + draggable: true, + childAddable: true, + deletable: true, + duplicable: true, + }, }; const renderComponent = (props) => { diff --git a/src/course-outline/data/selectors.js b/src/course-outline/data/selectors.js index 4e0c28375..ebfb71a91 100644 --- a/src/course-outline/data/selectors.js +++ b/src/course-outline/data/selectors.js @@ -6,3 +6,4 @@ export const getSectionsList = (state) => state.courseOutline.sectionsList; export const getCurrentItem = (state) => state.courseOutline.currentItem; export const getCurrentSection = (state) => state.courseOutline.currentSection; export const getCurrentSubsection = (state) => state.courseOutline.currentSubsection; +export const getCourseActions = (state) => state.courseOutline.actions; diff --git a/src/course-outline/data/slice.js b/src/course-outline/data/slice.js index c014cb43c..dca773468 100644 --- a/src/course-outline/data/slice.js +++ b/src/course-outline/data/slice.js @@ -31,6 +31,12 @@ const slice = createSlice({ currentSection: {}, currentSubsection: {}, currentItem: {}, + actions: { + deletable: true, + draggable: true, + childAddable: true, + duplicable: true, + }, }, reducers: { fetchOutlineIndexSuccess: (state, { payload }) => { @@ -61,6 +67,12 @@ const slice = createSlice({ ...payload, }; }, + updateCourseActions: (state, { payload }) => { + state.actions = { + ...state.actions, + ...payload, + }; + }, fetchStatusBarChecklistSuccess: (state, { payload }) => { state.statusBarData.checklist = { ...state.statusBarData.checklist, @@ -166,6 +178,7 @@ export const { updateOutlineIndexLoadingStatus, updateReindexLoadingStatus, updateStatusBar, + updateCourseActions, fetchStatusBarChecklistSuccess, fetchStatusBarSelPacedSuccess, updateFetchSectionLoadingStatus, diff --git a/src/course-outline/data/thunk.js b/src/course-outline/data/thunk.js index 8fdcc211b..4f7abcbf7 100644 --- a/src/course-outline/data/thunk.js +++ b/src/course-outline/data/thunk.js @@ -34,6 +34,7 @@ import { updateOutlineIndexLoadingStatus, updateReindexLoadingStatus, updateStatusBar, + updateCourseActions, fetchStatusBarChecklistSuccess, fetchStatusBarSelPacedSuccess, updateSavingStatus, @@ -59,6 +60,7 @@ export function fetchCourseOutlineIndexQuery(courseId) { highlightsEnabledForMessaging, videoSharingEnabled, videoSharingOptions, + actions, }, } = outlineIndex; dispatch(fetchOutlineIndexSuccess(outlineIndex)); @@ -68,6 +70,7 @@ export function fetchCourseOutlineIndexQuery(courseId) { videoSharingOptions, videoSharingEnabled, })); + dispatch(updateCourseActions(actions)); dispatch(updateOutlineIndexLoadingStatus({ status: RequestStatus.SUCCESSFUL })); } catch (error) { diff --git a/src/course-outline/drag-helper/ConditionalSortableElement.jsx b/src/course-outline/drag-helper/ConditionalSortableElement.jsx new file mode 100644 index 000000000..7873a8cc2 --- /dev/null +++ b/src/course-outline/drag-helper/ConditionalSortableElement.jsx @@ -0,0 +1,46 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { Row } from '@edx/paragon'; +import { SortableItem } from '@edx/frontend-lib-content-components'; + +const ConditionalSortableElement = ({ + id, + draggable, + children, + componentStyle, +}) => { + if (draggable) { + return ( + +
+ {children} +
+
+ ); + } + return ( + + {children} + + ); +}; + +ConditionalSortableElement.defaultProps = { + componentStyle: null, +}; + +ConditionalSortableElement.propTypes = { + id: PropTypes.string.isRequired, + draggable: PropTypes.bool.isRequired, + children: PropTypes.node.isRequired, + componentStyle: PropTypes.shape({}), +}; + +export default ConditionalSortableElement; diff --git a/src/course-outline/drag-helper/ConditionalSortableElement.scss b/src/course-outline/drag-helper/ConditionalSortableElement.scss new file mode 100644 index 000000000..4f0222975 --- /dev/null +++ b/src/course-outline/drag-helper/ConditionalSortableElement.scss @@ -0,0 +1,8 @@ +.extend-margin { + display: flex; + flex-grow: 1; + + .item-children { + margin-right: -2.75rem; + } +} diff --git a/src/course-outline/empty-placeholder/EmptyPlaceholder.jsx b/src/course-outline/empty-placeholder/EmptyPlaceholder.jsx index 746dabed8..4ccabe18c 100644 --- a/src/course-outline/empty-placeholder/EmptyPlaceholder.jsx +++ b/src/course-outline/empty-placeholder/EmptyPlaceholder.jsx @@ -6,35 +6,41 @@ import { Button, OverlayTrigger, Tooltip } from '@edx/paragon'; import messages from './messages'; -const EmptyPlaceholder = ({ onCreateNewSection }) => { +const EmptyPlaceholder = ({ + onCreateNewSection, + childAddable, +}) => { const intl = useIntl(); return (

{intl.formatMessage(messages.title)}

- - {intl.formatMessage(messages.tooltip)} - - )} - > - - + + + )}
); }; EmptyPlaceholder.propTypes = { onCreateNewSection: PropTypes.func.isRequired, + childAddable: PropTypes.bool.isRequired, }; export default EmptyPlaceholder; diff --git a/src/course-outline/empty-placeholder/EmptyPlaceholder.test.jsx b/src/course-outline/empty-placeholder/EmptyPlaceholder.test.jsx index f76a1178c..45c6841fd 100644 --- a/src/course-outline/empty-placeholder/EmptyPlaceholder.test.jsx +++ b/src/course-outline/empty-placeholder/EmptyPlaceholder.test.jsx @@ -9,7 +9,10 @@ const onCreateNewSectionMock = jest.fn(); const renderComponent = () => render( - + , ); diff --git a/src/course-outline/header-navigations/HeaderNavigations.jsx b/src/course-outline/header-navigations/HeaderNavigations.jsx index 5315085c3..d2f41b86c 100644 --- a/src/course-outline/header-navigations/HeaderNavigations.jsx +++ b/src/course-outline/header-navigations/HeaderNavigations.jsx @@ -16,6 +16,7 @@ const HeaderNavigations = ({ isSectionsExpanded, isDisabledReindexButton, hasSections, + courseActions, }) => { const intl = useIntl(); const { @@ -24,21 +25,23 @@ const HeaderNavigations = ({ return (