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 (