feat: [FC-0044] Course unit - Drag and drop for xblocks (#908)
Implements drag and drop for xblocks in the unit page.
This commit is contained in:
@@ -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';
|
||||
|
||||
|
||||
@@ -7,5 +7,4 @@
|
||||
@import "./empty-placeholder/EmptyPlaceholder";
|
||||
@import "./highlights-modal/HighlightsModal";
|
||||
@import "./publish-modal/PublishModal";
|
||||
@import "./drag-helper/SortableItem";
|
||||
@import "./xblock-status/XBlockStatus";
|
||||
|
||||
@@ -56,7 +56,7 @@ import {
|
||||
moveUnitOver,
|
||||
moveSubsection,
|
||||
moveUnit,
|
||||
} from './drag-helper/utils';
|
||||
} from '../generic/drag-helper/utils';
|
||||
|
||||
let axiosMock;
|
||||
let store;
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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 (
|
||||
<>
|
||||
<Container size="xl" className="course-unit px-4">
|
||||
@@ -122,6 +140,7 @@ const CourseUnit = ({ courseId }) => {
|
||||
<Layout.Element>
|
||||
{currentlyVisibleToStudents && (
|
||||
<AlertMessage
|
||||
className="course-unit__alert"
|
||||
title={intl.formatMessage(messages.alertUnpublishedVersion)}
|
||||
variant="warning"
|
||||
icon={WarningIcon}
|
||||
@@ -133,24 +152,36 @@ const CourseUnit = ({ courseId }) => {
|
||||
courseId={courseId}
|
||||
/>
|
||||
)}
|
||||
<Stack gap={4} className="mb-4">
|
||||
{courseVerticalChildren.children.map(({
|
||||
name, blockId: id, blockType: type, shouldScroll, userPartitionInfo, validationMessages,
|
||||
}) => (
|
||||
<CourseXBlock
|
||||
id={id}
|
||||
key={id}
|
||||
title={name}
|
||||
type={type}
|
||||
blockId={blockId}
|
||||
validationMessages={validationMessages}
|
||||
shouldScroll={shouldScroll}
|
||||
unitXBlockActions={unitXBlockActions}
|
||||
handleConfigureSubmit={handleConfigureSubmit}
|
||||
data-testid="course-xblock"
|
||||
userPartitionInfo={userPartitionInfo}
|
||||
/>
|
||||
))}
|
||||
<Stack className="mb-4 course-unit__xblocks">
|
||||
<DraggableList
|
||||
itemList={unitXBlocks}
|
||||
setState={setUnitXBlocks}
|
||||
updateOrder={finalizeXBlockOrder}
|
||||
>
|
||||
<SortableContext
|
||||
id="root"
|
||||
items={unitXBlocks}
|
||||
strategy={verticalListSortingStrategy}
|
||||
>
|
||||
{unitXBlocks.map(({
|
||||
name, id, blockType: type, shouldScroll, userPartitionInfo, validationMessages,
|
||||
}) => (
|
||||
<CourseXBlock
|
||||
id={id}
|
||||
key={id}
|
||||
title={name}
|
||||
type={type}
|
||||
blockId={blockId}
|
||||
validationMessages={validationMessages}
|
||||
shouldScroll={shouldScroll}
|
||||
handleConfigureSubmit={handleConfigureSubmit}
|
||||
unitXBlockActions={unitXBlockActions}
|
||||
data-testid="course-xblock"
|
||||
userPartitionInfo={userPartitionInfo}
|
||||
/>
|
||||
))}
|
||||
</SortableContext>
|
||||
</DraggableList>
|
||||
</Stack>
|
||||
<AddComponent
|
||||
blockId={blockId}
|
||||
|
||||
@@ -4,3 +4,7 @@
|
||||
@import "./course-xblock/CourseXBlock";
|
||||
@import "./sidebar/Sidebar";
|
||||
@import "./header-title/HeaderTitle";
|
||||
|
||||
.course-unit__alert {
|
||||
margin-bottom: 1.75rem;
|
||||
}
|
||||
|
||||
@@ -54,6 +54,7 @@ import addComponentMessages from './add-component/messages';
|
||||
import { PUBLISH_TYPES, UNIT_VISIBILITY_STATES } from './constants';
|
||||
import messages from './messages';
|
||||
import { getContentTaxonomyTagsApiUrl, getContentTaxonomyTagsCountApiUrl } from '../content-tags-drawer/data/api';
|
||||
import { RequestStatus } from '../data/constants';
|
||||
|
||||
let axiosMock;
|
||||
let store;
|
||||
@@ -1132,7 +1133,9 @@ describe('<CourseUnit />', () => {
|
||||
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('<CourseUnit />', () => {
|
||||
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('<CourseUnit />', () => {
|
||||
)).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Drag and drop', () => {
|
||||
it('checks xblock list is restored to original order when API call fails', async () => {
|
||||
const { findAllByRole } = render(<RootWrapper />);
|
||||
|
||||
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(<RootWrapper />);
|
||||
|
||||
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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -34,6 +34,7 @@ const PastNotificationAlert = ({ staticFileNotices, courseId }) => {
|
||||
{hasConflictingErrors && (
|
||||
<AlertMessage
|
||||
data-testid="has-conflicting-errors-alert"
|
||||
className="course-unit__alert"
|
||||
title={intl.formatMessage(messages.hasConflictingErrorsTitle)}
|
||||
onClose={() => handleCloseNotificationAlert('conflictingFilesAlert')}
|
||||
description={(
|
||||
@@ -56,6 +57,7 @@ const PastNotificationAlert = ({ staticFileNotices, courseId }) => {
|
||||
{hasErrorFiles && (
|
||||
<AlertMessage
|
||||
data-testid="has-error-files-alert"
|
||||
className="course-unit__alert"
|
||||
title={intl.formatMessage(messages.hasErrorsTitle)}
|
||||
onClose={() => handleCloseNotificationAlert('errorFilesAlert')}
|
||||
description={(
|
||||
@@ -72,6 +74,7 @@ const PastNotificationAlert = ({ staticFileNotices, courseId }) => {
|
||||
{hasNewFiles && (
|
||||
<AlertMessage
|
||||
data-testid="has-new-files-alert"
|
||||
className="course-unit__alert"
|
||||
title={intl.formatMessage(messages.hasNewFilesTitle)}
|
||||
onClose={() => 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;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useEffect, useRef } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import classNames from 'classnames';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import {
|
||||
ActionRow, Card, Dropdown, Icon, IconButton, useToggle,
|
||||
@@ -11,6 +12,7 @@ import { useNavigate, useSearchParams } from 'react-router-dom';
|
||||
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';
|
||||
@@ -77,18 +79,25 @@ const CourseXBlock = ({
|
||||
<div
|
||||
ref={courseXBlockElementRef}
|
||||
{...props}
|
||||
className={isScrolledToElement ? 'xblock-highlight' : undefined}
|
||||
className={classNames('course-unit__xblock', {
|
||||
'xblock-highlight': isScrolledToElement,
|
||||
})}
|
||||
>
|
||||
<Card className="mb-1">
|
||||
<Card
|
||||
as={SortableItem}
|
||||
id={id}
|
||||
draggable
|
||||
category="xblock"
|
||||
componentStyle={{ marginBottom: 0 }}
|
||||
>
|
||||
<Card.Header
|
||||
title={title}
|
||||
subtitle={visibilityMessage}
|
||||
actions={(
|
||||
<ActionRow>
|
||||
<ActionRow className="mr-2">
|
||||
<IconButton
|
||||
alt={intl.formatMessage(messages.blockAltButtonEdit)}
|
||||
iconAs={EditIcon}
|
||||
size="md"
|
||||
onClick={handleEdit}
|
||||
/>
|
||||
<Dropdown>
|
||||
@@ -97,7 +106,6 @@ const CourseXBlock = ({
|
||||
as={IconButton}
|
||||
src={MoveVertIcon}
|
||||
alt={intl.formatMessage(messages.blockActionsDropdownAlt)}
|
||||
size="sm"
|
||||
iconAs={Icon}
|
||||
/>
|
||||
<Dropdown.Menu>
|
||||
@@ -135,7 +143,6 @@ const CourseXBlock = ({
|
||||
/>
|
||||
</ActionRow>
|
||||
)}
|
||||
size="md"
|
||||
/>
|
||||
<Card.Section>
|
||||
<XBlockMessages validationMessages={validationMessages} />
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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<Object>} - 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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -50,3 +50,38 @@ 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 {
|
||||
// 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;
|
||||
};
|
||||
|
||||
@@ -11,13 +11,14 @@ import {
|
||||
fetchCourseVerticalChildrenData,
|
||||
deleteUnitItemQuery,
|
||||
duplicateUnitItemQuery,
|
||||
setXBlockOrderListQuery,
|
||||
editCourseUnitVisibilityAndData,
|
||||
} from './data/thunk';
|
||||
import {
|
||||
getCourseSectionVertical,
|
||||
getCourseVerticalChildren,
|
||||
getCourseUnitData,
|
||||
getLoadingStatus,
|
||||
getIsLoading,
|
||||
getSavingStatus,
|
||||
getSequenceStatus,
|
||||
getStaticFileNotices,
|
||||
@@ -37,7 +38,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);
|
||||
@@ -111,6 +112,10 @@ export const useCourseUnit = ({ courseId, blockId }) => {
|
||||
},
|
||||
};
|
||||
|
||||
const handleXBlockDragAndDrop = (xblockListIds, restoreCallback) => {
|
||||
dispatch(setXBlockOrderListQuery(blockId, xblockListIds, restoreCallback));
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (savingStatus === RequestStatus.SUCCESSFUL) {
|
||||
dispatch(updateQueryPendingStatus(true));
|
||||
@@ -137,8 +142,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,
|
||||
@@ -152,6 +156,7 @@ export const useCourseUnit = ({ courseId, blockId }) => {
|
||||
handleCreateNewCourseXBlock,
|
||||
handleConfigureSubmit,
|
||||
courseVerticalChildren,
|
||||
handleXBlockDragAndDrop,
|
||||
canPasteComponent,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
@@ -11,3 +11,4 @@
|
||||
@import "./tag-count/TagCount";
|
||||
@import "./modal-dropzone/ModalDropzone";
|
||||
@import "./configure-modal/ConfigureModal";
|
||||
@import "./drag-helper/SortableItem";
|
||||
|
||||
Reference in New Issue
Block a user