This handles loading errors when opening the course unit page via direct link as an unauthorized user.
340 lines
11 KiB
JavaScript
340 lines
11 KiB
JavaScript
import {
|
|
useCallback, useEffect, useMemo, useRef, useState,
|
|
} from 'react';
|
|
import { useDispatch, useSelector } from 'react-redux';
|
|
import { useNavigate, useSearchParams } from 'react-router-dom';
|
|
import { useToggle } from '@openedx/paragon';
|
|
import { camelCaseObject } from '@edx/frontend-platform/utils';
|
|
|
|
import { RequestStatus } from '../data/constants';
|
|
import { useClipboard } from '../generic/clipboard';
|
|
import { useEventListener } from '../generic/hooks';
|
|
import { COURSE_BLOCK_NAMES, iframeMessageTypes } from '../constants';
|
|
import { messageTypes, PUBLISH_TYPES } from './constants';
|
|
import {
|
|
createNewCourseXBlock,
|
|
deleteUnitItemQuery,
|
|
duplicateUnitItemQuery,
|
|
editCourseItemQuery,
|
|
editCourseUnitVisibilityAndData,
|
|
fetchCourseSectionVerticalData,
|
|
fetchCourseVerticalChildrenData,
|
|
getCourseOutlineInfoQuery,
|
|
patchUnitItemQuery,
|
|
} from './data/thunk';
|
|
import {
|
|
getCanEdit,
|
|
getCourseOutlineInfo,
|
|
getCourseSectionVertical,
|
|
getCourseUnitData,
|
|
getCourseVerticalChildren,
|
|
getErrorMessage,
|
|
getIsLoading,
|
|
getMovedXBlockParams,
|
|
getSavingStatus,
|
|
getSequenceStatus,
|
|
getStaticFileNotices,
|
|
} from './data/selectors';
|
|
import {
|
|
changeEditTitleFormOpen,
|
|
updateMovedXBlockParams,
|
|
updateQueryPendingStatus,
|
|
} from './data/slice';
|
|
import { useIframe } from '../generic/hooks/context/hooks';
|
|
|
|
export const useCourseUnit = ({ courseId, blockId }) => {
|
|
const dispatch = useDispatch();
|
|
const [searchParams] = useSearchParams();
|
|
const { sendMessageToIframe } = useIframe();
|
|
const [addComponentTemplateData, setAddComponentTemplateData] = useState({});
|
|
const [isMoveModalOpen, openMoveModal, closeMoveModal] = useToggle(false);
|
|
|
|
const courseUnit = useSelector(getCourseUnitData);
|
|
const savingStatus = useSelector(getSavingStatus);
|
|
const isLoading = useSelector(getIsLoading);
|
|
const errorMessage = useSelector(getErrorMessage);
|
|
const sequenceStatus = useSelector(getSequenceStatus);
|
|
const { draftPreviewLink, publishedPreviewLink, xblockInfo = {} } = useSelector(getCourseSectionVertical);
|
|
const courseVerticalChildren = useSelector(getCourseVerticalChildren);
|
|
const staticFileNotices = useSelector(getStaticFileNotices);
|
|
const navigate = useNavigate();
|
|
const isTitleEditFormOpen = useSelector(state => state.courseUnit.isTitleEditFormOpen);
|
|
const canEdit = useSelector(getCanEdit);
|
|
const courseOutlineInfo = useSelector(getCourseOutlineInfo);
|
|
const movedXBlockParams = useSelector(getMovedXBlockParams);
|
|
const { currentlyVisibleToStudents } = courseUnit;
|
|
const { sharedClipboardData, showPasteXBlock, showPasteUnit } = useClipboard(canEdit);
|
|
const { canPasteComponent } = courseVerticalChildren;
|
|
const { displayName: unitTitle, category: unitCategory } = xblockInfo;
|
|
const sequenceId = courseUnit.ancestorInfo?.ancestors[0].id;
|
|
const isUnitVerticalType = unitCategory === COURSE_BLOCK_NAMES.vertical.id;
|
|
const isUnitLibraryType = unitCategory === COURSE_BLOCK_NAMES.libraryContent.id;
|
|
const isSplitTestType = unitCategory === COURSE_BLOCK_NAMES.splitTest.id;
|
|
|
|
const headerNavigationsActions = {
|
|
handleViewLive: () => {
|
|
window.open(publishedPreviewLink, '_blank');
|
|
},
|
|
handlePreview: () => {
|
|
window.open(draftPreviewLink, '_blank');
|
|
},
|
|
handleEdit: () => {
|
|
sendMessageToIframe(messageTypes.editXBlock, { id: courseUnit.id }, window);
|
|
},
|
|
};
|
|
|
|
const handleTitleEdit = () => {
|
|
dispatch(changeEditTitleFormOpen(!isTitleEditFormOpen));
|
|
};
|
|
|
|
const handleConfigureSubmit = (id, isVisible, groupAccess, isDiscussionEnabled, closeModalFn) => {
|
|
dispatch(editCourseUnitVisibilityAndData(
|
|
id,
|
|
PUBLISH_TYPES.republish,
|
|
isVisible,
|
|
groupAccess,
|
|
isDiscussionEnabled,
|
|
() => sendMessageToIframe(messageTypes.completeManageXBlockAccess, { locator: id }),
|
|
blockId,
|
|
));
|
|
if (typeof closeModalFn === 'function') {
|
|
closeModalFn();
|
|
}
|
|
};
|
|
|
|
const handleTitleEditSubmit = (displayName) => {
|
|
if (unitTitle !== displayName) {
|
|
dispatch(editCourseItemQuery(blockId, displayName, sequenceId));
|
|
}
|
|
|
|
handleTitleEdit();
|
|
};
|
|
|
|
const handleNavigate = (id) => {
|
|
if (sequenceId) {
|
|
const path = `/course/${courseId}/container/${blockId}/${id}`;
|
|
const options = { replace: true };
|
|
if (searchParams.size) {
|
|
navigate({
|
|
pathname: path,
|
|
search: `?${searchParams}`,
|
|
}, options);
|
|
} else {
|
|
navigate(path, options);
|
|
}
|
|
}
|
|
};
|
|
|
|
const handleCreateNewCourseXBlock = (body, callback) => (
|
|
dispatch(createNewCourseXBlock(body, callback, blockId, sendMessageToIframe))
|
|
);
|
|
|
|
const unitXBlockActions = {
|
|
handleDelete: (XBlockId) => {
|
|
dispatch(deleteUnitItemQuery(blockId, XBlockId, sendMessageToIframe));
|
|
},
|
|
handleDuplicate: (XBlockId) => {
|
|
dispatch(duplicateUnitItemQuery(
|
|
blockId,
|
|
XBlockId,
|
|
(courseKey, locator) => sendMessageToIframe(messageTypes.completeXBlockDuplicating, { courseKey, locator }),
|
|
));
|
|
},
|
|
};
|
|
|
|
const handleRollbackMovedXBlock = () => {
|
|
const {
|
|
sourceLocator, targetParentLocator, title, currentParentLocator,
|
|
} = movedXBlockParams;
|
|
dispatch(patchUnitItemQuery({
|
|
sourceLocator,
|
|
targetParentLocator,
|
|
title,
|
|
currentParentLocator,
|
|
isMoving: false,
|
|
callbackFn: () => {
|
|
sendMessageToIframe(messageTypes.rollbackMovedXBlock, { locator: sourceLocator });
|
|
window.scrollTo({ top: 0, behavior: 'smooth' });
|
|
},
|
|
}));
|
|
};
|
|
|
|
const handleCloseXBlockMovedAlert = () => {
|
|
dispatch(updateMovedXBlockParams({ isSuccess: false }));
|
|
};
|
|
|
|
const handleNavigateToTargetUnit = () => {
|
|
navigate(`/course/${courseId}/container/${movedXBlockParams.targetParentLocator}`);
|
|
};
|
|
|
|
const receiveMessage = useCallback(({ data }) => {
|
|
const { payload, type } = data;
|
|
|
|
if (type === messageTypes.handleViewXBlockContent) {
|
|
const { usageId } = payload;
|
|
navigate(`/course/${courseId}/container/${usageId}/${sequenceId}`);
|
|
}
|
|
|
|
if (type === messageTypes.handleViewGroupConfigurations) {
|
|
const { usageId } = payload;
|
|
const groupId = usageId.split('#').pop();
|
|
navigate(`/course/${courseId}/group_configurations#${groupId}`);
|
|
}
|
|
|
|
if (type === messageTypes.showComponentTemplates) {
|
|
setAddComponentTemplateData(camelCaseObject(payload));
|
|
}
|
|
}, [courseId, sequenceId]);
|
|
|
|
useEventListener('message', receiveMessage);
|
|
|
|
useEffect(() => {
|
|
if (savingStatus === RequestStatus.SUCCESSFUL) {
|
|
dispatch(updateQueryPendingStatus(true));
|
|
}
|
|
}, [savingStatus]);
|
|
|
|
useEffect(() => {
|
|
dispatch(fetchCourseSectionVerticalData(blockId, sequenceId));
|
|
dispatch(fetchCourseVerticalChildrenData(blockId, isSplitTestType));
|
|
handleNavigate(sequenceId);
|
|
dispatch(updateMovedXBlockParams({ isSuccess: false }));
|
|
}, [courseId, blockId, sequenceId]);
|
|
|
|
useEffect(() => {
|
|
if (isSplitTestType) {
|
|
dispatch(fetchCourseVerticalChildrenData(blockId, isSplitTestType));
|
|
}
|
|
}, [isSplitTestType, blockId]);
|
|
|
|
useEffect(() => {
|
|
if (isMoveModalOpen && !Object.keys(courseOutlineInfo).length) {
|
|
dispatch(getCourseOutlineInfoQuery(courseId));
|
|
}
|
|
}, [isMoveModalOpen]);
|
|
|
|
return {
|
|
sequenceId,
|
|
courseUnit,
|
|
unitTitle,
|
|
unitCategory,
|
|
errorMessage,
|
|
sequenceStatus,
|
|
savingStatus,
|
|
staticFileNotices,
|
|
currentlyVisibleToStudents,
|
|
isLoading,
|
|
isTitleEditFormOpen,
|
|
isUnitVerticalType,
|
|
isUnitLibraryType,
|
|
isSplitTestType,
|
|
sharedClipboardData,
|
|
showPasteXBlock,
|
|
showPasteUnit,
|
|
unitXBlockActions,
|
|
headerNavigationsActions,
|
|
handleTitleEdit,
|
|
handleTitleEditSubmit,
|
|
handleCreateNewCourseXBlock,
|
|
handleConfigureSubmit,
|
|
courseVerticalChildren,
|
|
canPasteComponent,
|
|
isMoveModalOpen,
|
|
openMoveModal,
|
|
closeMoveModal,
|
|
handleRollbackMovedXBlock,
|
|
handleCloseXBlockMovedAlert,
|
|
movedXBlockParams,
|
|
handleNavigateToTargetUnit,
|
|
addComponentTemplateData,
|
|
setAddComponentTemplateData,
|
|
};
|
|
};
|
|
|
|
/**
|
|
* Custom hook to determine the layout grid configuration based on unit category and type.
|
|
*
|
|
* @param {string} unitCategory - The category of the unit. This may influence future layout logic.
|
|
* @param {boolean} isUnitLibraryType - A flag indicating whether the unit is of library content type.
|
|
* @returns {Object} - An object representing the layout configuration for different screen sizes.
|
|
* The configuration includes keys like 'lg', 'md', 'sm', 'xs', and 'xl',
|
|
* each specifying an array of layout spans.
|
|
*/
|
|
export const useLayoutGrid = (unitCategory, isUnitLibraryType) => (
|
|
useMemo(() => {
|
|
const layouts = {
|
|
fullWidth: {
|
|
lg: [{ span: 12 }, { span: 0 }],
|
|
md: [{ span: 12 }, { span: 0 }],
|
|
sm: [{ span: 12 }, { span: 0 }],
|
|
xs: [{ span: 12 }, { span: 0 }],
|
|
xl: [{ span: 12 }, { span: 0 }],
|
|
},
|
|
default: {
|
|
lg: [{ span: 8 }, { span: 4 }],
|
|
md: [{ span: 8 }, { span: 4 }],
|
|
sm: [{ span: 8 }, { span: 3 }],
|
|
xs: [{ span: 9 }, { span: 3 }],
|
|
xl: [{ span: 9 }, { span: 3 }],
|
|
},
|
|
};
|
|
|
|
return isUnitLibraryType ? layouts.fullWidth : layouts.default;
|
|
}, [unitCategory])
|
|
);
|
|
|
|
/**
|
|
* Custom hook that restores the scroll position from `localStorage` after a page reload.
|
|
* It listens for a `plugin.resize` message event and scrolls the window to the saved position
|
|
* after a 1-second delay, provided no new resize messages are received during that time.
|
|
*
|
|
* @param {string} [storageKey='createXBlockLastYPosition'] -
|
|
* The key used to store the last scroll position in `localStorage`.
|
|
*/
|
|
export const useScrollToLastPosition = (storageKey = 'createXBlockLastYPosition') => {
|
|
const timeoutRef = useRef(null);
|
|
const [hasLastPosition, setHasLastPosition] = useState(() => !!localStorage.getItem(storageKey));
|
|
|
|
const scrollToLastPosition = useCallback(() => {
|
|
const lastYPosition = localStorage.getItem(storageKey);
|
|
if (!lastYPosition) {
|
|
setHasLastPosition(false);
|
|
return;
|
|
}
|
|
|
|
const yPosition = parseInt(lastYPosition, 10);
|
|
if (!Number.isNaN(yPosition)) {
|
|
window.scrollTo({ top: yPosition, behavior: 'smooth' });
|
|
localStorage.removeItem(storageKey);
|
|
setHasLastPosition(false);
|
|
}
|
|
}, [storageKey]);
|
|
|
|
const handleMessage = useCallback((event) => {
|
|
if (event.data?.type === iframeMessageTypes.resize) {
|
|
if (timeoutRef.current) {
|
|
clearTimeout(timeoutRef.current);
|
|
}
|
|
|
|
timeoutRef.current = setTimeout(scrollToLastPosition, 1000);
|
|
}
|
|
}, [scrollToLastPosition]);
|
|
|
|
useEffect(() => {
|
|
if (!hasLastPosition) {
|
|
return undefined;
|
|
}
|
|
|
|
window.addEventListener('message', handleMessage);
|
|
|
|
return () => {
|
|
window.removeEventListener('message', handleMessage);
|
|
if (timeoutRef.current) {
|
|
clearTimeout(timeoutRef.current);
|
|
}
|
|
};
|
|
}, [hasLastPosition, handleMessage]);
|
|
|
|
return null;
|
|
};
|