From b69884301c0a1394b3c5afb522687ddb31acb3bc Mon Sep 17 00:00:00 2001 From: Peter Kulko <93188219+PKulkoRaccoonGang@users.noreply.github.com> Date: Thu, 15 Feb 2024 17:13:54 +0200 Subject: [PATCH] feat: [AXIMST-46] Course unit - Drag And Drop For Xblocks (#158) * feat: [AXIMST-46] Drag And Drop For Xblocks * feat: added tests * refactor: refactoring after review --- src/course-outline/CourseOutline.jsx | 4 +- src/course-outline/CourseOutline.scss | 1 - src/course-outline/CourseOutline.test.jsx | 2 +- .../section-card/SectionCard.jsx | 4 +- .../subsection-card/SubsectionCard.jsx | 4 +- src/course-outline/unit-card/UnitCard.jsx | 2 +- src/course-unit/CourseUnit.jsx | 72 +++++++++++++------ src/course-unit/CourseUnit.scss | 4 ++ src/course-unit/CourseUnit.test.jsx | 65 +++++++++++++++-- .../clipboard/paste-notification/index.jsx | 18 +++-- .../course-xblock/CourseXBlock.jsx | 14 ++-- .../course-xblock/CourseXBlock.scss | 4 ++ src/course-unit/data/api.js | 18 ++++- src/course-unit/data/selectors.js | 4 +- src/course-unit/data/slice.js | 11 ++- src/course-unit/data/thunk.js | 25 +++++++ src/course-unit/data/utils.js | 37 ++++++++++ src/course-unit/hooks.jsx | 13 ++-- .../drag-helper/DragContextProvider.jsx | 0 .../drag-helper/DraggableList.jsx | 2 +- .../drag-helper/SortableItem.jsx | 0 .../drag-helper/SortableItem.scss | 0 .../drag-helper/messages.js | 0 .../drag-helper/utils.js | 0 src/generic/styles.scss | 1 + 25 files changed, 251 insertions(+), 54 deletions(-) rename src/{course-outline => generic}/drag-helper/DragContextProvider.jsx (100%) rename src/{course-outline => generic}/drag-helper/DraggableList.jsx (99%) rename src/{course-outline => generic}/drag-helper/SortableItem.jsx (100%) rename src/{course-outline => generic}/drag-helper/SortableItem.scss (100%) rename src/{course-outline => generic}/drag-helper/messages.js (100%) rename src/{course-outline => generic}/drag-helper/utils.js (100%) diff --git a/src/course-outline/CourseOutline.jsx b/src/course-outline/CourseOutline.jsx index fc1581687d..0ebbdf3497 100644 --- a/src/course-outline/CourseOutline.jsx +++ b/src/course-outline/CourseOutline.jsx @@ -45,12 +45,12 @@ import HighlightsModal from './highlights-modal/HighlightsModal'; import EmptyPlaceholder from './empty-placeholder/EmptyPlaceholder'; import PublishModal from './publish-modal/PublishModal'; import PageAlerts from './page-alerts/PageAlerts'; -import DraggableList from './drag-helper/DraggableList'; +import DraggableList from '../generic/drag-helper/DraggableList'; import { canMoveSection, possibleUnitMoves, possibleSubsectionMoves, -} from './drag-helper/utils'; +} from '../generic/drag-helper/utils'; import { useCourseOutline } from './hooks'; import messages from './messages'; diff --git a/src/course-outline/CourseOutline.scss b/src/course-outline/CourseOutline.scss index 19bdb37c40..4744444caa 100644 --- a/src/course-outline/CourseOutline.scss +++ b/src/course-outline/CourseOutline.scss @@ -7,7 +7,6 @@ @import "./empty-placeholder/EmptyPlaceholder"; @import "./highlights-modal/HighlightsModal"; @import "./publish-modal/PublishModal"; -@import "./drag-helper/SortableItem"; @import "./xblock-status/XBlockStatus"; div.row:has(> div > div.highlight) { diff --git a/src/course-outline/CourseOutline.test.jsx b/src/course-outline/CourseOutline.test.jsx index ce85ec1b9a..d59231ccf4 100644 --- a/src/course-outline/CourseOutline.test.jsx +++ b/src/course-outline/CourseOutline.test.jsx @@ -56,7 +56,7 @@ import { moveUnitOver, moveSubsection, moveUnit, -} from './drag-helper/utils'; +} from '../generic/drag-helper/utils'; let axiosMock; let store; diff --git a/src/course-outline/section-card/SectionCard.jsx b/src/course-outline/section-card/SectionCard.jsx index 914d201ab8..55089158fd 100644 --- a/src/course-outline/section-card/SectionCard.jsx +++ b/src/course-outline/section-card/SectionCard.jsx @@ -13,8 +13,8 @@ import classNames from 'classnames'; import { setCurrentItem, setCurrentSection } from '../data/slice'; import { RequestStatus } from '../../data/constants'; import CardHeader from '../card-header/CardHeader'; -import SortableItem from '../drag-helper/SortableItem'; -import { DragContext } from '../drag-helper/DragContextProvider'; +import SortableItem from '../../generic/drag-helper/SortableItem'; +import { DragContext } from '../../generic/drag-helper/DragContextProvider'; import TitleButton from '../card-header/TitleButton'; import XBlockStatus from '../xblock-status/XBlockStatus'; import { getItemStatus, getItemStatusBorder, scrollToElement } from '../utils'; diff --git a/src/course-outline/subsection-card/SubsectionCard.jsx b/src/course-outline/subsection-card/SubsectionCard.jsx index 7f31b4b1fe..12bee0aec3 100644 --- a/src/course-outline/subsection-card/SubsectionCard.jsx +++ b/src/course-outline/subsection-card/SubsectionCard.jsx @@ -14,8 +14,8 @@ import { isEmpty } from 'lodash'; import { setCurrentItem, setCurrentSection, setCurrentSubsection } from '../data/slice'; import { RequestStatus } from '../../data/constants'; import CardHeader from '../card-header/CardHeader'; -import SortableItem from '../drag-helper/SortableItem'; -import { DragContext } from '../drag-helper/DragContextProvider'; +import SortableItem from '../../generic/drag-helper/SortableItem'; +import { DragContext } from '../../generic/drag-helper/DragContextProvider'; import { useCopyToClipboard, PasteComponent } from '../../generic/clipboard'; import TitleButton from '../card-header/TitleButton'; import XBlockStatus from '../xblock-status/XBlockStatus'; diff --git a/src/course-outline/unit-card/UnitCard.jsx b/src/course-outline/unit-card/UnitCard.jsx index 79193056c0..469f28e791 100644 --- a/src/course-outline/unit-card/UnitCard.jsx +++ b/src/course-outline/unit-card/UnitCard.jsx @@ -9,7 +9,7 @@ import { useSearchParams } from 'react-router-dom'; import { setCurrentItem, setCurrentSection, setCurrentSubsection } from '../data/slice'; import { RequestStatus } from '../../data/constants'; import CardHeader from '../card-header/CardHeader'; -import SortableItem from '../drag-helper/SortableItem'; +import SortableItem from '../../generic/drag-helper/SortableItem'; import TitleLink from '../card-header/TitleLink'; import XBlockStatus from '../xblock-status/XBlockStatus'; import { getItemStatus, getItemStatusBorder, scrollToElement } from '../utils'; diff --git a/src/course-unit/CourseUnit.jsx b/src/course-unit/CourseUnit.jsx index 57f5e52174..75d9029163 100644 --- a/src/course-unit/CourseUnit.jsx +++ b/src/course-unit/CourseUnit.jsx @@ -1,10 +1,12 @@ +import { useEffect, useMemo, useState } from 'react'; import PropTypes from 'prop-types'; import { useSelector } from 'react-redux'; import { useParams } from 'react-router-dom'; import { Container, Layout, Stack } from '@openedx/paragon'; import { useIntl, injectIntl } from '@edx/frontend-platform/i18n'; -import { ErrorAlert } from '@edx/frontend-lib-content-components'; +import { DraggableList, ErrorAlert } from '@edx/frontend-lib-content-components'; import { Warning as WarningIcon } from '@openedx/paragon/icons'; +import { SortableContext, verticalListSortingStrategy } from '@dnd-kit/sortable'; import { getProcessingNotification } from '../generic/processing-notification/data/selectors'; import SubHeader from '../generic/sub-header/SubHeader'; @@ -56,10 +58,20 @@ const CourseUnit = ({ courseId }) => { handleCreateNewCourseXBlock, handleConfigureSubmit, courseVerticalChildren, + handleXBlockDragAndDrop, canPasteComponent, } = useCourseUnit({ courseId, blockId }); - document.title = getPageHeadTitle('', unitTitle); + const initialXBlocksData = useMemo(() => courseVerticalChildren.children ?? [], [courseVerticalChildren.children]); + const [unitXBlocks, setUnitXBlocks] = useState(initialXBlocksData); + + useEffect(() => { + document.title = getPageHeadTitle('', unitTitle); + }, [unitTitle]); + + useEffect(() => { + setUnitXBlocks(courseVerticalChildren.children); + }, [courseVerticalChildren.children]); const { isShow: isShowProcessingNotification, @@ -78,6 +90,12 @@ const CourseUnit = ({ courseId }) => { ); } + const finalizeXBlockOrder = () => (newXBlocks) => { + handleXBlockDragAndDrop(newXBlocks.map(xBlock => xBlock.id), () => { + setUnitXBlocks(initialXBlocksData); + }); + }; + return ( <> @@ -122,6 +140,7 @@ const CourseUnit = ({ courseId }) => { {currentlyVisibleToStudents && ( { courseId={courseId} /> )} - - {courseVerticalChildren.children.map(({ - name, blockId: id, blockType: type, shouldScroll, userPartitionInfo, validationMessages, - }) => ( - - ))} + + + + {unitXBlocks.map(({ + name, id, blockType: type, shouldScroll, userPartitionInfo, validationMessages, + }) => ( + + ))} + + ', () => { userEvent.click(getByRole('button', { name: sidebarMessages.actionButtonCopyUnitTitle.defaultMessage })); userEvent.click(getByRole('button', { name: messages.pasteButtonText.defaultMessage })); - expect(getAllByTestId('course-xblock')).toHaveLength(2); + await waitFor(() => { + expect(getAllByTestId('course-xblock')).toHaveLength(2); + }); axiosMock .onGet(getCourseVerticalChildrenApiUrl(blockId)) @@ -1219,9 +1222,11 @@ describe('', () => { courseUnitMock, ]); - units = getAllByTestId('course-unit-btn'); - const courseUnits = courseSectionVerticalMock.xblock_info.ancestor_info.ancestors[0].child_info.children; - expect(units).toHaveLength(courseUnits.length); + await waitFor(() => { + units = getAllByTestId('course-unit-btn'); + const courseUnits = courseSectionVerticalMock.xblock_info.ancestor_info.ancestors[0].child_info.children; + expect(units).toHaveLength(courseUnits.length); + }); axiosMock .onPost(postXBlockBaseApiUrl(), postXBlockBody) @@ -1442,4 +1447,56 @@ describe('', () => { )).not.toBeInTheDocument(); }); }); + + describe('Drag and drop', () => { + it('checks xblock list is restored to original order when API call fails', async () => { + const { findAllByRole } = render(); + + const xBlocksDraggers = await findAllByRole('button', { name: 'Drag to reorder' }); + const draggableButton = xBlocksDraggers[1]; + + axiosMock + .onPut(getXBlockBaseApiUrl(blockId)) + .reply(500, { dummy: 'value' }); + + const xBlock1 = store.getState().courseUnit.courseVerticalChildren.children[0].id; + + fireEvent.keyDown(draggableButton, { key: 'ArrowUp' }); + + await waitFor(async () => { + fireEvent.keyDown(draggableButton, { code: 'Space' }); + + const saveStatus = store.getState().courseUnit.savingStatus; + expect(saveStatus).toEqual(RequestStatus.FAILED); + }); + + const xBlock1New = store.getState().courseUnit.courseVerticalChildren.children[0].id; + expect(xBlock1).toBe(xBlock1New); + }); + + it('check that new xblock list is saved when dragged', async () => { + const { findAllByRole } = render(); + + const xBlocksDraggers = await findAllByRole('button', { name: 'Drag to reorder' }); + const draggableButton = xBlocksDraggers[1]; + + axiosMock + .onPut(getXBlockBaseApiUrl(blockId)) + .reply(200, { dummy: 'value' }); + + const xBlock1 = store.getState().courseUnit.courseVerticalChildren.children[0].id; + + fireEvent.keyDown(draggableButton, { key: 'ArrowUp' }); + + await waitFor(async () => { + fireEvent.keyDown(draggableButton, { code: 'Space' }); + + const saveStatus = store.getState().courseUnit.savingStatus; + expect(saveStatus).toEqual(RequestStatus.SUCCESSFUL); + }); + + const xBlock2 = store.getState().courseUnit.courseVerticalChildren.children[1].id; + expect(xBlock1).toBe(xBlock2); + }); + }); }); diff --git a/src/course-unit/clipboard/paste-notification/index.jsx b/src/course-unit/clipboard/paste-notification/index.jsx index 260acdd20a..b92334c717 100644 --- a/src/course-unit/clipboard/paste-notification/index.jsx +++ b/src/course-unit/clipboard/paste-notification/index.jsx @@ -34,6 +34,7 @@ const PastNotificationAlert = ({ staticFileNotices, courseId }) => { {hasConflictingErrors && ( handleCloseNotificationAlert('conflictingFilesAlert')} description={( @@ -56,6 +57,7 @@ const PastNotificationAlert = ({ staticFileNotices, courseId }) => { {hasErrorFiles && ( handleCloseNotificationAlert('errorFilesAlert')} description={( @@ -72,6 +74,7 @@ const PastNotificationAlert = ({ staticFileNotices, courseId }) => { {hasNewFiles && ( handleCloseNotificationAlert('newFilesAlert')} description={( @@ -97,11 +100,16 @@ const PastNotificationAlert = ({ staticFileNotices, courseId }) => { PastNotificationAlert.propTypes = { courseId: PropTypes.string.isRequired, - staticFileNotices: PropTypes.shape({ - conflictingFiles: PropTypes.arrayOf(PropTypes.string), - errorFiles: PropTypes.arrayOf(PropTypes.string), - newFiles: PropTypes.arrayOf(PropTypes.string), - }).isRequired, + staticFileNotices: + PropTypes.objectOf({ + conflictingFiles: PropTypes.arrayOf(PropTypes.string), + errorFiles: PropTypes.arrayOf(PropTypes.string), + newFiles: PropTypes.arrayOf(PropTypes.string), + }), +}; + +PastNotificationAlert.defaultProps = { + staticFileNotices: {}, }; export default PastNotificationAlert; diff --git a/src/course-unit/course-xblock/CourseXBlock.jsx b/src/course-unit/course-xblock/CourseXBlock.jsx index 46ba76e32a..6589b47e6f 100644 --- a/src/course-unit/course-xblock/CourseXBlock.jsx +++ b/src/course-unit/course-xblock/CourseXBlock.jsx @@ -11,6 +11,7 @@ import { useIntl } from '@edx/frontend-platform/i18n'; import { getCanEdit, getCourseId } from 'CourseAuthoring/course-unit/data/selectors'; import DeleteModal from '../../generic/delete-modal/DeleteModal'; import ConfigureModal from '../../generic/configure-modal/ConfigureModal'; +import SortableItem from '../../generic/drag-helper/SortableItem'; import { scrollToElement } from '../../course-outline/utils'; import { COURSE_BLOCK_NAMES } from '../../constants'; import { copyToClipboard } from '../../generic/data/thunks'; @@ -71,16 +72,21 @@ const CourseXBlock = ({ return (
- + + @@ -89,7 +95,6 @@ const CourseXBlock = ({ as={IconButton} src={MoveVertIcon} alt={intl.formatMessage(messages.blockActionsDropdownAlt)} - size="sm" iconAs={Icon} /> @@ -127,7 +132,6 @@ const CourseXBlock = ({ /> )} - size="md" /> diff --git a/src/course-unit/course-xblock/CourseXBlock.scss b/src/course-unit/course-xblock/CourseXBlock.scss index 262d19d653..4ae9f6dab1 100644 --- a/src/course-unit/course-xblock/CourseXBlock.scss +++ b/src/course-unit/course-xblock/CourseXBlock.scss @@ -1,5 +1,9 @@ .course-unit { .course-unit__xblocks { + .course-unit__xblock:not(:first-child) { + margin-top: 1.75rem; + } + .pgn__card-header { display: flex; justify-content: space-between; diff --git a/src/course-unit/data/api.js b/src/course-unit/data/api.js index d21a95b786..155e9d9878 100644 --- a/src/course-unit/data/api.js +++ b/src/course-unit/data/api.js @@ -3,7 +3,7 @@ import { camelCaseObject, getConfig } from '@edx/frontend-platform'; import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth'; import { PUBLISH_TYPES } from '../constants'; -import { normalizeCourseSectionVerticalData } from './utils'; +import { normalizeCourseSectionVerticalData, updateXBlockBlockIdToId } from './utils'; const getStudioBaseUrl = () => getConfig().STUDIO_BASE_URL; @@ -116,8 +116,9 @@ export async function handleCourseUnitVisibilityAndData(unitId, type, isVisible, export async function getCourseVerticalChildren(itemId) { const { data } = await getAuthenticatedHttpClient() .get(getCourseVerticalChildrenApiUrl(itemId)); + const camelCaseData = camelCaseObject(data); - return camelCaseObject(data); + return updateXBlockBlockIdToId(camelCaseData); } /** @@ -147,3 +148,16 @@ export async function duplicateUnitItem(itemId, XBlockId) { return data; } + +/** + * Sets the order list of XBlocks. + * @param {string} blockId - The identifier of the course unit. + * @param {Object[]} children - The array of child elements representing the updated order of XBlocks. + * @returns {Promise} - A promise that resolves to the updated data after setting the XBlock order. + */ +export async function setXBlockOrderList(blockId, children) { + const { data } = await getAuthenticatedHttpClient() + .put(getXBlockBaseApiUrl(blockId), { children }); + + return data; +} diff --git a/src/course-unit/data/selectors.js b/src/course-unit/data/selectors.js index e9e98bc0ce..41cb7ea912 100644 --- a/src/course-unit/data/selectors.js +++ b/src/course-unit/data/selectors.js @@ -1,13 +1,11 @@ import { createSelector } from '@reduxjs/toolkit'; - -import { RequestStatus } from '../../data/constants'; +import { RequestStatus } from 'CourseAuthoring/data/constants'; export const getCourseUnitData = (state) => state.courseUnit.unit; export const getCanEdit = (state) => state.courseUnit.canEdit; export const getStaticFileNotices = (state) => state.courseUnit.staticFileNotices; export const getCourseUnit = (state) => state.courseUnit; export const getSavingStatus = (state) => state.courseUnit.savingStatus; -export const getLoadingStatus = (state) => state.courseUnit.loadingStatus; export const getSequenceStatus = (state) => state.courseUnit.sequenceStatus; export const getSequenceIds = (state) => state.courseUnit.courseSectionVertical.courseSequenceIds; export const getCourseSectionVertical = (state) => state.courseUnit.courseSectionVertical; diff --git a/src/course-unit/data/slice.js b/src/course-unit/data/slice.js index 02edc09757..24c1b965cd 100644 --- a/src/course-unit/data/slice.js +++ b/src/course-unit/data/slice.js @@ -83,7 +83,7 @@ const slice = createSlice({ }, deleteXBlock: (state, { payload }) => { state.courseVerticalChildren.children = state.courseVerticalChildren.children.filter( - (component) => component.blockId !== payload, + (component) => component.id !== payload, ); }, duplicateXBlock: (state, { payload }) => { @@ -100,6 +100,14 @@ const slice = createSlice({ fetchStaticFileNoticesSuccess: (state, { payload }) => { state.staticFileNotices = payload; }, + reorderXBlockList: (state, { payload }) => { + // Create a map for payload IDs to their index for O(1) lookups + const indexMap = new Map(payload.map((id, index) => [id, index])); + + // Directly sort the children based on the order defined in payload + // This avoids the need to copy the array beforehand + state.courseVerticalChildren.children.sort((a, b) => (indexMap.get(a.id) || 0) - (indexMap.get(b.id) || 0)); + }, }, }); @@ -121,6 +129,7 @@ export const { deleteXBlock, duplicateXBlock, fetchStaticFileNoticesSuccess, + reorderXBlockList, } = slice.actions; export const { diff --git a/src/course-unit/data/thunk.js b/src/course-unit/data/thunk.js index e94287c03c..109e121c7e 100644 --- a/src/course-unit/data/thunk.js +++ b/src/course-unit/data/thunk.js @@ -17,6 +17,7 @@ import { handleCourseUnitVisibilityAndData, deleteUnitItem, duplicateUnitItem, + setXBlockOrderList, } from './api'; import { updateLoadingCourseUnitStatus, @@ -34,6 +35,7 @@ import { deleteXBlock, duplicateXBlock, fetchStaticFileNoticesSuccess, + reorderXBlockList, } from './slice'; import { getNotificationMessage } from './utils'; @@ -246,3 +248,26 @@ export function duplicateUnitItemQuery(itemId, xblockId) { } }; } + +export function setXBlockOrderListQuery(blockId, xblockListIds, restoreCallback) { + return async (dispatch) => { + dispatch(updateSavingStatus({ status: RequestStatus.PENDING })); + dispatch(showProcessingNotification(NOTIFICATION_MESSAGES.saving)); + + try { + await setXBlockOrderList(blockId, xblockListIds).then(async (result) => { + if (result) { + dispatch(reorderXBlockList(xblockListIds)); + dispatch(updateSavingStatus({ status: RequestStatus.SUCCESSFUL })); + const courseUnit = await getCourseUnitData(blockId); + dispatch(fetchCourseItemSuccess(courseUnit)); + } + }); + } catch (error) { + restoreCallback(); + dispatch(updateSavingStatus({ status: RequestStatus.FAILED })); + } finally { + dispatch(hideProcessingNotification()); + } + }; +} diff --git a/src/course-unit/data/utils.js b/src/course-unit/data/utils.js index 49223e1a7d..852ab57aee 100644 --- a/src/course-unit/data/utils.js +++ b/src/course-unit/data/utils.js @@ -50,3 +50,40 @@ export const getNotificationMessage = (type, isVisible, isModalView) => { return notificationMessage; }; + +/** + * Updates the 'id' property of objects in the data structure using the 'blockId' value where present. + * @param {Object} data - The original data structure to be updated. + * @returns {Object} - The updated data structure with updated 'id' values. + */ +export const updateXBlockBlockIdToId = (data) => { + if (typeof data !== 'object' || data === null) { + return data; + } + + if (Array.isArray(data)) { + return data.map(updateXBlockBlockIdToId); + } + + const updatedData = {}; + + Object.keys(data).forEach(key => { + const value = data[key]; + + if (key === 'children' || key === 'selectablePartitions' || key === 'groups') { + updatedData[key] = updateXBlockBlockIdToId(value); + } else if (key === 'blockId') { + updatedData.id = value; + } else { + // Copy other properties unchanged + updatedData[key] = value; + } + }); + + // Special handling for objects with both 'id' and 'blockId' to ensure 'blockId' takes precedence + if ('blockId' in data) { + updatedData.id = data.blockId; + } + + return updatedData; +}; diff --git a/src/course-unit/hooks.jsx b/src/course-unit/hooks.jsx index 335593db2a..de990636a7 100644 --- a/src/course-unit/hooks.jsx +++ b/src/course-unit/hooks.jsx @@ -11,13 +11,14 @@ import { fetchCourseVerticalChildrenData, deleteUnitItemQuery, duplicateUnitItemQuery, + setXBlockOrderListQuery, editCourseUnitVisibilityAndData, } from './data/thunk'; import { getCourseSectionVertical, getCourseVerticalChildren, getCourseUnitData, - getLoadingStatus, + getIsLoading, getSavingStatus, getSequenceStatus, getStaticFileNotices, @@ -36,7 +37,7 @@ export const useCourseUnit = ({ courseId, blockId }) => { const [hasInternetConnectionError, setInternetConnectionError] = useState(false); const courseUnit = useSelector(getCourseUnitData); const savingStatus = useSelector(getSavingStatus); - const loadingStatus = useSelector(getLoadingStatus); + const isLoading = useSelector(getIsLoading); const sequenceStatus = useSelector(getSequenceStatus); const { draftPreviewLink, publishedPreviewLink } = useSelector(getCourseSectionVertical); const courseVerticalChildren = useSelector(getCourseVerticalChildren); @@ -101,6 +102,10 @@ export const useCourseUnit = ({ courseId, blockId }) => { }, }; + const handleXBlockDragAndDrop = (xblockListIds, restoreCallback) => { + dispatch(setXBlockOrderListQuery(blockId, xblockListIds, restoreCallback)); + }; + useEffect(() => { if (savingStatus === RequestStatus.SUCCESSFUL) { dispatch(updateQueryPendingStatus(true)); @@ -127,8 +132,7 @@ export const useCourseUnit = ({ courseId, blockId }) => { isErrorAlert, staticFileNotices, currentlyVisibleToStudents, - isLoading: loadingStatus.fetchUnitLoadingStatus === RequestStatus.IN_PROGRESS - || loadingStatus.courseSectionVerticalLoadingStatus === RequestStatus.IN_PROGRESS, + isLoading, isTitleEditFormOpen, isInternetConnectionAlertFailed: savingStatus === RequestStatus.FAILED, sharedClipboardData, @@ -142,6 +146,7 @@ export const useCourseUnit = ({ courseId, blockId }) => { handleCreateNewCourseXBlock, handleConfigureSubmit, courseVerticalChildren, + handleXBlockDragAndDrop, canPasteComponent, }; }; diff --git a/src/course-outline/drag-helper/DragContextProvider.jsx b/src/generic/drag-helper/DragContextProvider.jsx similarity index 100% rename from src/course-outline/drag-helper/DragContextProvider.jsx rename to src/generic/drag-helper/DragContextProvider.jsx diff --git a/src/course-outline/drag-helper/DraggableList.jsx b/src/generic/drag-helper/DraggableList.jsx similarity index 99% rename from src/course-outline/drag-helper/DraggableList.jsx rename to src/generic/drag-helper/DraggableList.jsx index 04d006d9cc..0b62b9aee3 100644 --- a/src/course-outline/drag-helper/DraggableList.jsx +++ b/src/generic/drag-helper/DraggableList.jsx @@ -16,7 +16,7 @@ import { import { restrictToVerticalAxis } from '@dnd-kit/modifiers'; import DragContextProvider from './DragContextProvider'; -import { COURSE_BLOCK_NAMES } from '../constants'; +import { COURSE_BLOCK_NAMES } from '../../constants'; import { moveSubsectionOver, moveUnitOver, diff --git a/src/course-outline/drag-helper/SortableItem.jsx b/src/generic/drag-helper/SortableItem.jsx similarity index 100% rename from src/course-outline/drag-helper/SortableItem.jsx rename to src/generic/drag-helper/SortableItem.jsx diff --git a/src/course-outline/drag-helper/SortableItem.scss b/src/generic/drag-helper/SortableItem.scss similarity index 100% rename from src/course-outline/drag-helper/SortableItem.scss rename to src/generic/drag-helper/SortableItem.scss diff --git a/src/course-outline/drag-helper/messages.js b/src/generic/drag-helper/messages.js similarity index 100% rename from src/course-outline/drag-helper/messages.js rename to src/generic/drag-helper/messages.js diff --git a/src/course-outline/drag-helper/utils.js b/src/generic/drag-helper/utils.js similarity index 100% rename from src/course-outline/drag-helper/utils.js rename to src/generic/drag-helper/utils.js diff --git a/src/generic/styles.scss b/src/generic/styles.scss index 9c74c5b8bc..43a9973a41 100644 --- a/src/generic/styles.scss +++ b/src/generic/styles.scss @@ -11,3 +11,4 @@ @import "./tag-count/TagCount"; @import "./modal-dropzone/ModalDropzone"; @import "./configure-modal/ConfigureModal"; +@import "./drag-helper/SortableItem";