From b417cd64a0339bd926ad89924638eee54c03e092 Mon Sep 17 00:00:00 2001 From: Navin Karkera Date: Mon, 8 Jan 2024 19:47:41 +0530 Subject: [PATCH] feat: use actions and other flags to control item actions Uses action flags from API to control display of delete, duplicate, child new button and dragging. Use isHeaderVisible flag to control display of subsection headers. All these changes prepare outline for entrance exam section display. feat: use actions flags for subsections test: actions --- src/course-outline/CourseOutline.jsx | 43 +++++++---- src/course-outline/CourseOutline.scss | 1 + src/course-outline/CourseOutline.test.jsx | 32 ++++++++ src/course-outline/card-header/CardHeader.jsx | 35 ++++++--- .../card-header/CardHeader.test.jsx | 6 ++ src/course-outline/data/selectors.js | 1 + src/course-outline/data/slice.js | 13 ++++ src/course-outline/data/thunk.js | 3 + .../ConditionalSortableElement.jsx | 46 ++++++++++++ .../ConditionalSortableElement.scss | 8 ++ .../empty-placeholder/EmptyPlaceholder.jsx | 40 +++++----- .../EmptyPlaceholder.test.jsx | 5 +- .../header-navigations/HeaderNavigations.jsx | 37 +++++---- .../HeaderNavigations.test.jsx | 8 ++ src/course-outline/hooks.jsx | 3 + .../section-card/SectionCard.jsx | 75 ++++++++++++------- .../section-card/SectionCard.scss | 1 - .../section-card/SectionCard.test.jsx | 41 +++++++++- .../subsection-card/SubsectionCard.jsx | 70 ++++++++++------- .../subsection-card/SubsectionCard.scss | 1 - .../subsection-card/SubsectionCard.test.jsx | 41 +++++++++- src/course-outline/unit-card/UnitCard.jsx | 15 ++++ .../unit-card/UnitCard.test.jsx | 40 +++++++++- 23 files changed, 443 insertions(+), 122 deletions(-) create mode 100644 src/course-outline/drag-helper/ConditionalSortableElement.jsx create mode 100644 src/course-outline/drag-helper/ConditionalSortableElement.scss 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 (