chore: iframe rendering optimization (#1544)

Iframe reload optimizations for various xblock related actions. Added some improvements related to scrolling to the current xblock. Fixed behavior of the xblock action dropdown list.
This commit is contained in:
Peter Kulko
2025-02-11 11:31:07 -08:00
committed by GitHub
parent b0fc3d923b
commit e9130d3852
18 changed files with 145 additions and 106 deletions

1
.gitignore vendored
View File

@@ -1,6 +1,7 @@
.DS_Store
.eslintcache
.idea
.run
node_modules
npm-debug.log
coverage

View File

@@ -247,7 +247,7 @@ describe('<CourseUnit />', () => {
);
simulatePostMessageEvent(messageTypes.deleteXBlock, {
id: courseVerticalChildrenMock.children[0].block_id,
usageId: courseVerticalChildrenMock.children[0].block_id,
});
expect(getByText(/Delete this component?/i)).toBeInTheDocument();
@@ -261,10 +261,10 @@ describe('<CourseUnit />', () => {
const deleteButton = getAllByRole('button', { name: /Delete/i })
.find(({ classList }) => classList.contains('btn-primary'));
userEvent.click(cancelButton);
expect(cancelButton).toBeInTheDocument();
simulatePostMessageEvent(messageTypes.deleteXBlock, {
id: courseVerticalChildrenMock.children[0].block_id,
usageId: courseVerticalChildrenMock.children[0].block_id,
});
expect(getByRole('dialog')).toBeInTheDocument();
@@ -300,8 +300,12 @@ describe('<CourseUnit />', () => {
axiosMock
.onDelete(getXBlockBaseApiUrl(courseVerticalChildrenMock.children[0].block_id))
.replyOnce(200, { dummy: 'value' });
await executeThunk(deleteUnitItemQuery(courseId, blockId), store.dispatch);
.reply(200, { dummy: 'value' });
await executeThunk(deleteUnitItemQuery(
courseId,
courseVerticalChildrenMock.children[0].block_id,
simulatePostMessageEvent,
), store.dispatch);
const updatedCourseVerticalChildren = courseVerticalChildrenMock.children.filter(
child => child.block_id !== courseVerticalChildrenMock.children[0].block_id,
@@ -1632,6 +1636,8 @@ describe('<CourseUnit />', () => {
callbackFn: requestData.callbackFn,
}), store.dispatch);
simulatePostMessageEvent(messageTypes.rollbackMovedXBlock, { locator: requestData.sourceLocator });
const dismissButton = queryByRole('button', {
name: /dismiss/i, hidden: true,
});

View File

@@ -61,6 +61,7 @@ const AddComponent = ({ blockId, handleCreateNewCourseXBlock }) => {
case COMPONENT_TYPES.problem:
case COMPONENT_TYPES.video:
handleCreateNewCourseXBlock({ type, parentLocator: blockId }, ({ courseKey, locator }) => {
localStorage.setItem('modalEditLastYPosition', window.scrollY);
navigate(`/course/${courseKey}/editor/${type}/${locator}`);
});
break;

View File

@@ -52,15 +52,21 @@ export const messageTypes = {
videoFullScreen: 'plugin.videoFullScreen',
refreshXBlock: 'refreshXBlock',
showMoveXBlockModal: 'showMoveXBlockModal',
completeXBlockMoving: 'completeXBlockMoving',
rollbackMovedXBlock: 'rollbackMovedXBlock',
showMultipleComponentPicker: 'showMultipleComponentPicker',
addSelectedComponentsToBank: 'addSelectedComponentsToBank',
showXBlockLibraryChangesPreview: 'showXBlockLibraryChangesPreview',
copyXBlock: 'copyXBlock',
manageXBlockAccess: 'manageXBlockAccess',
completeManageXBlockAccess: 'completeManageXBlockAccess',
deleteXBlock: 'deleteXBlock',
completeXBlockDeleting: 'completeXBlockDeleting',
duplicateXBlock: 'duplicateXBlock',
refreshXBlockPositions: 'refreshPositions',
completeXBlockDuplicating: 'completeXBlockDuplicating',
newXBlockEditor: 'newXBlockEditor',
toggleCourseXBlockDropdown: 'toggleCourseXBlockDropdown',
addXBlock: 'addXBlock',
scrollToXBlock: 'scrollToXBlock',
handleViewXBlockContent: 'handleViewXBlockContent',
};

View File

@@ -9,6 +9,7 @@ import { RequestStatus } from '../../data/constants';
import { NOTIFICATION_MESSAGES } from '../../constants';
import { updateModel, updateModels } from '../../generic/model-store';
import { updateClipboardData } from '../../generic/data/slice';
import { messageTypes } from '../constants';
import {
getCourseUnitData,
editUnitDisplayName,
@@ -126,6 +127,7 @@ export function editCourseUnitVisibilityAndData(
isVisible,
groupAccess,
isDiscussionEnabled,
callback,
blockId = itemId,
) {
return async (dispatch) => {
@@ -143,6 +145,9 @@ export function editCourseUnitVisibilityAndData(
isDiscussionEnabled,
).then(async (result) => {
if (result) {
if (callback) {
callback();
}
const courseUnit = await getCourseUnitData(blockId);
dispatch(fetchCourseItemSuccess(courseUnit));
const courseVerticalChildrenData = await getCourseVerticalChildren(blockId);
@@ -158,11 +163,8 @@ export function editCourseUnitVisibilityAndData(
};
}
export function createNewCourseXBlock(body, callback, blockId) {
export function createNewCourseXBlock(body, callback, blockId, sendMessageToIframe) {
return async (dispatch) => {
dispatch(updateLoadingCourseXblockStatus({ status: RequestStatus.IN_PROGRESS }));
dispatch(updateSavingStatus({ status: RequestStatus.PENDING }));
if (body.stagedContent) {
dispatch(showProcessingNotification(NOTIFICATION_MESSAGES.pasting));
} else {
@@ -188,10 +190,10 @@ export function createNewCourseXBlock(body, callback, blockId) {
const courseVerticalChildrenData = await getCourseVerticalChildren(blockId);
dispatch(updateCourseVerticalChildren(courseVerticalChildrenData));
dispatch(hideProcessingNotification());
dispatch(updateLoadingCourseXblockStatus({ status: RequestStatus.SUCCESSFUL }));
dispatch(updateSavingStatus({ status: RequestStatus.SUCCESSFUL }));
if (callback) {
callback(result);
} else {
sendMessageToIframe(messageTypes.addXBlock, { data: result });
}
const currentBlockId = body.category === 'vertical' ? formattedResult.locator : blockId;
const courseUnit = await getCourseUnitData(currentBlockId);
@@ -220,13 +222,14 @@ export function fetchCourseVerticalChildrenData(itemId) {
};
}
export function deleteUnitItemQuery(itemId, xblockId) {
export function deleteUnitItemQuery(itemId, xblockId, sendMessageToIframe) {
return async (dispatch) => {
dispatch(updateSavingStatus({ status: RequestStatus.PENDING }));
dispatch(showProcessingNotification(NOTIFICATION_MESSAGES.deleting));
try {
await deleteUnitItem(xblockId);
sendMessageToIframe(messageTypes.completeXBlockDeleting, { locator: xblockId });
const { userClipboard } = await getCourseSectionVerticalData(itemId);
dispatch(updateClipboardData(userClipboard));
const courseUnit = await getCourseUnitData(itemId);
@@ -240,13 +243,14 @@ export function deleteUnitItemQuery(itemId, xblockId) {
};
}
export function duplicateUnitItemQuery(itemId, xblockId) {
export function duplicateUnitItemQuery(itemId, xblockId, callback) {
return async (dispatch) => {
dispatch(updateSavingStatus({ status: RequestStatus.PENDING }));
dispatch(showProcessingNotification(NOTIFICATION_MESSAGES.duplicating));
try {
await duplicateUnitItem(itemId, xblockId);
const { courseKey, locator } = await duplicateUnitItem(itemId, xblockId);
callback(courseKey, locator);
const courseUnit = await getCourseUnitData(itemId);
dispatch(fetchCourseItemSuccess(courseUnit));
dispatch(hideProcessingNotification());
@@ -300,9 +304,13 @@ export function patchUnitItemQuery({
dispatch(updateMovedXBlockParams(xBlockParams));
dispatch(updateCourseOutlineInfo({}));
dispatch(updateCourseOutlineInfoLoadingStatus({ status: RequestStatus.IN_PROGRESS }));
const courseUnit = await getCourseUnitData(currentParentLocator);
dispatch(fetchCourseItemSuccess(courseUnit));
callbackFn();
try {
const courseUnit = await getCourseUnitData(currentParentLocator);
dispatch(fetchCourseItemSuccess(courseUnit));
} catch (error) {
handleResponseErrors(error, dispatch, updateSavingStatus);
}
callbackFn(sourceLocator);
} catch (error) {
handleResponseErrors(error, dispatch, updateSavingStatus);
} finally {

View File

@@ -12,8 +12,6 @@ import ConfigureModal from '../../generic/configure-modal/ConfigureModal';
import { COURSE_BLOCK_NAMES } from '../../constants';
import { getCourseUnitData } from '../data/selectors';
import { updateQueryPendingStatus } from '../data/slice';
import { messageTypes } from '../constants';
import { useIframe } from '../context/hooks';
import messages from './messages';
const HeaderTitle = ({
@@ -29,15 +27,9 @@ const HeaderTitle = ({
const currentItemData = useSelector(getCourseUnitData);
const [isConfigureModalOpen, openConfigureModal, closeConfigureModal] = useToggle(false);
const { selectedPartitionIndex, selectedGroupsLabel } = currentItemData.userPartitionInfo;
const { sendMessageToIframe } = useIframe();
const onConfigureSubmit = (...arg) => {
handleConfigureSubmit(currentItemData.id, ...arg, closeConfigureModal);
// TODO: this artificial delay is a temporary solution
// to ensure the iframe content is properly refreshed.
setTimeout(() => {
sendMessageToIframe(messageTypes.refreshXBlock, null);
}, 1000);
};
const getVisibilityMessage = () => {

View File

@@ -60,11 +60,9 @@ describe('<HeaderTitle />', () => {
it('render HeaderTitle component correctly', () => {
const { getByText, getByRole } = renderComponent();
waitFor(() => {
expect(getByText(unitTitle)).toBeInTheDocument();
expect(getByRole('button', { name: messages.altButtonEdit.defaultMessage })).toBeInTheDocument();
expect(getByRole('button', { name: messages.altButtonSettings.defaultMessage })).toBeInTheDocument();
});
expect(getByText(unitTitle)).toBeInTheDocument();
expect(getByRole('button', { name: messages.altButtonEdit.defaultMessage })).toBeInTheDocument();
expect(getByRole('button', { name: messages.altButtonSettings.defaultMessage })).toBeInTheDocument();
});
it('render HeaderTitle with open edit form', () => {
@@ -72,41 +70,35 @@ describe('<HeaderTitle />', () => {
isTitleEditFormOpen: true,
});
waitFor(() => {
expect(getByRole('textbox', { name: messages.ariaLabelButtonEdit.defaultMessage })).toBeInTheDocument();
expect(getByRole('textbox', { name: messages.ariaLabelButtonEdit.defaultMessage })).toHaveValue(unitTitle);
expect(getByRole('button', { name: messages.altButtonEdit.defaultMessage })).toBeInTheDocument();
expect(getByRole('button', { name: messages.altButtonSettings.defaultMessage })).toBeInTheDocument();
});
expect(getByRole('textbox', { name: messages.ariaLabelButtonEdit.defaultMessage })).toBeInTheDocument();
expect(getByRole('textbox', { name: messages.ariaLabelButtonEdit.defaultMessage })).toHaveValue(unitTitle);
expect(getByRole('button', { name: messages.altButtonEdit.defaultMessage })).toBeInTheDocument();
expect(getByRole('button', { name: messages.altButtonSettings.defaultMessage })).toBeInTheDocument();
});
it('calls toggle edit title form by clicking on Edit button', () => {
const { getByRole } = renderComponent();
waitFor(() => {
const editTitleButton = getByRole('button', { name: messages.altButtonEdit.defaultMessage });
userEvent.click(editTitleButton);
expect(handleTitleEdit).toHaveBeenCalledTimes(1);
});
const editTitleButton = getByRole('button', { name: messages.altButtonEdit.defaultMessage });
userEvent.click(editTitleButton);
expect(handleTitleEdit).toHaveBeenCalledTimes(1);
});
it('calls saving title by clicking outside or press Enter key', async () => {
it('calls saving title by clicking outside or press Enter key', () => {
const { getByRole } = renderComponent({
isTitleEditFormOpen: true,
});
waitFor(() => {
const titleField = getByRole('textbox', { name: messages.ariaLabelButtonEdit.defaultMessage });
userEvent.type(titleField, ' 1');
expect(titleField).toHaveValue(`${unitTitle} 1`);
userEvent.click(document.body);
expect(handleTitleEditSubmit).toHaveBeenCalledTimes(1);
const titleField = getByRole('textbox', { name: messages.ariaLabelButtonEdit.defaultMessage });
userEvent.type(titleField, ' 1');
expect(titleField).toHaveValue(`${unitTitle} 1`);
userEvent.click(document.body);
expect(handleTitleEditSubmit).toHaveBeenCalledTimes(1);
userEvent.click(titleField);
userEvent.type(titleField, ' 2[Enter]');
expect(titleField).toHaveValue(`${unitTitle} 1 2`);
expect(handleTitleEditSubmit).toHaveBeenCalledTimes(2);
});
userEvent.click(titleField);
userEvent.type(titleField, ' 2[Enter]');
expect(titleField).toHaveValue(`${unitTitle} 1 2`);
expect(handleTitleEditSubmit).toHaveBeenCalledTimes(2);
});
it('displays a visibility message with the selected groups for the unit', async () => {
@@ -125,7 +117,7 @@ describe('<HeaderTitle />', () => {
const visibilityMessage = messages.definedVisibilityMessage.defaultMessage
.replace('{selectedGroupsLabel}', 'Visibility group 1');
waitFor(() => {
await waitFor(() => {
expect(getByText(visibilityMessage)).toBeInTheDocument();
});
});
@@ -140,8 +132,8 @@ describe('<HeaderTitle />', () => {
await executeThunk(fetchCourseUnitQuery(blockId), store.dispatch);
const { getByText } = renderComponent();
waitFor(() => {
expect(getByText(messages.someVisibilityMessage.defaultMessage)).toBeInTheDocument();
await waitFor(() => {
expect(getByText(messages.commonVisibilityMessage.defaultMessage)).toBeInTheDocument();
});
});
});

View File

@@ -88,6 +88,7 @@ export const useCourseUnit = ({ courseId, blockId }) => {
isVisible,
groupAccess,
isDiscussionEnabled,
() => sendMessageToIframe(messageTypes.completeManageXBlockAccess, { locator: id }),
blockId,
));
if (typeof closeModalFn === 'function') {
@@ -119,15 +120,19 @@ export const useCourseUnit = ({ courseId, blockId }) => {
};
const handleCreateNewCourseXBlock = (body, callback) => (
dispatch(createNewCourseXBlock(body, callback, blockId))
dispatch(createNewCourseXBlock(body, callback, blockId, sendMessageToIframe))
);
const unitXBlockActions = {
handleDelete: (XBlockId) => {
dispatch(deleteUnitItemQuery(blockId, XBlockId));
dispatch(deleteUnitItemQuery(blockId, XBlockId, sendMessageToIframe));
},
handleDuplicate: (XBlockId) => {
dispatch(duplicateUnitItemQuery(blockId, XBlockId));
dispatch(duplicateUnitItemQuery(
blockId,
XBlockId,
(courseKey, locator) => sendMessageToIframe(messageTypes.completeXBlockDuplicating, { courseKey, locator }),
));
},
};
@@ -142,7 +147,7 @@ export const useCourseUnit = ({ courseId, blockId }) => {
currentParentLocator,
isMoving: false,
callbackFn: () => {
sendMessageToIframe(messageTypes.refreshXBlock, null);
sendMessageToIframe(messageTypes.rollbackMovedXBlock, { locator: sourceLocator });
window.scrollTo({ top: 0, behavior: 'smooth' });
},
}));

View File

@@ -184,8 +184,8 @@ export const useMoveModal = ({
title: state.sourceXBlockInfo.current.displayName,
currentParentLocator: blockId,
isMoving: true,
callbackFn: () => {
sendMessageToIframe(messageTypes.refreshXBlock, null);
callbackFn: (sourceLocator: string) => {
sendMessageToIframe(messageTypes.completeXBlockMoving, { locator: sourceLocator });
closeModal();
window.scrollTo({ top: 0, behavior: 'smooth' });
},

View File

@@ -35,12 +35,14 @@ const PublishControls = ({ blockId }) => {
const handleCourseUnitDiscardChanges = () => {
closeDiscardModal();
dispatch(editCourseUnitVisibilityAndData(blockId, PUBLISH_TYPES.discardChanges));
// TODO: this artificial delay is a temporary solution
// to ensure the iframe content is properly refreshed.
setTimeout(() => {
sendMessageToIframe(messageTypes.refreshXBlock, null);
}, 1000);
dispatch(editCourseUnitVisibilityAndData(
blockId,
PUBLISH_TYPES.discardChanges,
null,
null,
null,
() => sendMessageToIframe(messageTypes.refreshXBlock, null),
));
};
const handleCourseUnitPublish = () => {

View File

@@ -4,7 +4,9 @@ import { useKeyedState } from '@edx/react-unit-test-utils';
import { logError } from '@edx/frontend-platform/logging';
import { stateKeys, messageTypes } from '../../../constants';
import { useLoadBearingHook, useIFrameBehavior } from '..';
import { useLoadBearingHook, useIFrameBehavior, useMessageHandlers } from '..';
jest.useFakeTimers();
jest.mock('@edx/react-unit-test-utils', () => ({
useKeyedState: jest.fn(),
@@ -171,3 +173,36 @@ describe('useLoadBearingHook', () => {
expect(setValue.mock.calls);
});
});
describe('useMessageHandlers', () => {
it('calls handleScrollToXBlock after debounce delay', () => {
const mockHandleScrollToXBlock = jest.fn();
const courseId = 'course-v1:Test+101+2025';
const navigate = jest.fn();
const dispatch = jest.fn();
const setIframeOffset = jest.fn();
const handleDeleteXBlock = jest.fn();
const handleDuplicateXBlock = jest.fn();
const handleManageXBlockAccess = jest.fn();
const { result } = renderHook(() => useMessageHandlers({
courseId,
navigate,
dispatch,
setIframeOffset,
handleDeleteXBlock,
handleDuplicateXBlock,
handleScrollToXBlock: mockHandleScrollToXBlock,
handleManageXBlockAccess,
}));
act(() => {
result.current[messageTypes.scrollToXBlock]({ scrollOffset: 200 });
});
jest.advanceTimersByTime(3000);
expect(mockHandleScrollToXBlock).toHaveBeenCalledTimes(1);
expect(mockHandleScrollToXBlock).toHaveBeenCalledWith(200);
});
});

View File

@@ -4,7 +4,7 @@ export type UseMessageHandlersTypes = {
dispatch: (action: any) => void;
setIframeOffset: (height: number) => void;
handleDeleteXBlock: (usageId: string) => void;
handleRefetchXBlocks: () => void;
handleScrollToXBlock: (scrollOffset: number) => void;
handleDuplicateXBlock: (blockType: string, usageId: string) => void;
handleManageXBlockAccess: (usageId: string) => void;
};

View File

@@ -1,6 +1,4 @@
import { useEffect, useCallback, RefObject } from 'react';
import { messageTypes } from '../../constants';
import { useEffect, RefObject } from 'react';
/**
* Hook for managing iframe content and providing utilities to interact with the iframe.
@@ -8,26 +6,15 @@ import { messageTypes } from '../../constants';
* @param {React.RefObject<HTMLIFrameElement>} iframeRef - A React ref for the iframe element.
* @param {(ref: React.RefObject<HTMLIFrameElement>) => void} setIframeRef -
* A function to associate the iframeRef with the parent context.
* @param {(type: string, payload: any) => void} sendMessageToIframe - A function to send messages to the iframe.
*
* @returns {Object} - An object containing utility functions.
* @returns {() => void} return.refreshIframeContent -
* A function to refresh the iframe content by sending a specific message.
* @returns {() => void}
*/
export const useIframeContent = (
iframeRef: RefObject<HTMLIFrameElement>,
setIframeRef: (ref: RefObject<HTMLIFrameElement>) => void,
sendMessageToIframe: (type: string, payload: any) => void,
): { refreshIframeContent: () => void } => {
): void => {
useEffect(() => {
setIframeRef(iframeRef);
}, [setIframeRef, iframeRef]);
// TODO: this artificial delay is a temporary solution
// to ensure the iframe content is properly refreshed.
const refreshIframeContent = useCallback(() => {
setTimeout(() => sendMessageToIframe(messageTypes.refreshXBlock, null), 1000);
}, [sendMessageToIframe]);
return { refreshIframeContent };
};

View File

@@ -1,4 +1,5 @@
import { useMemo } from 'react';
import { debounce } from 'lodash';
import { copyToClipboard } from '../../../generic/data/thunks';
import { messageTypes } from '../../constants';
@@ -16,8 +17,8 @@ export const useMessageHandlers = ({
dispatch,
setIframeOffset,
handleDeleteXBlock,
handleRefetchXBlocks,
handleDuplicateXBlock,
handleScrollToXBlock,
handleManageXBlockAccess,
}: UseMessageHandlersTypes): MessageHandlersTypes => useMemo(() => ({
[messageTypes.copyXBlock]: ({ usageId }) => dispatch(copyToClipboard(usageId)),
@@ -25,14 +26,14 @@ export const useMessageHandlers = ({
[messageTypes.newXBlockEditor]: ({ blockType, usageId }) => navigate(`/course/${courseId}/editor/${blockType}/${usageId}`),
[messageTypes.duplicateXBlock]: ({ blockType, usageId }) => handleDuplicateXBlock(blockType, usageId),
[messageTypes.manageXBlockAccess]: ({ usageId }) => handleManageXBlockAccess(usageId),
[messageTypes.refreshXBlockPositions]: handleRefetchXBlocks,
[messageTypes.scrollToXBlock]: debounce(({ scrollOffset }) => handleScrollToXBlock(scrollOffset), 3000),
[messageTypes.toggleCourseXBlockDropdown]: ({
courseXBlockDropdownHeight,
}: { courseXBlockDropdownHeight: number }) => setIframeOffset(courseXBlockDropdownHeight),
}), [
courseId,
handleDeleteXBlock,
handleRefetchXBlocks,
handleDuplicateXBlock,
handleManageXBlockAccess,
handleScrollToXBlock,
]);

View File

@@ -10,7 +10,6 @@ import DeleteModal from '../../generic/delete-modal/DeleteModal';
import ConfigureModal from '../../generic/configure-modal/ConfigureModal';
import { IFRAME_FEATURE_POLICY } from '../../constants';
import supportedEditors from '../../editors/supportedEditors';
import { fetchCourseUnitQuery } from '../data/thunk';
import { useIframe } from '../context/hooks';
import {
useMessageHandlers,
@@ -43,9 +42,10 @@ const XBlockContainerIframe: FC<XBlockContainerIframeProps> = ({
const iframeUrl = useMemo(() => getIframeUrl(blockId), [blockId]);
const { setIframeRef, sendMessageToIframe } = useIframe();
const { setIframeRef } = useIframe();
const { iframeHeight } = useIFrameBehavior({ id: blockId, iframeUrl });
const { refreshIframeContent } = useIframeContent(iframeRef, setIframeRef, sendMessageToIframe);
useIframeContent(iframeRef, setIframeRef);
useEffect(() => {
setIframeRef(iframeRef);
@@ -57,9 +57,8 @@ const XBlockContainerIframe: FC<XBlockContainerIframeProps> = ({
if (supportedEditors[blockType]) {
navigate(`/course/${courseId}/editor/${blockType}/${usageId}`);
}
refreshIframeContent();
},
[unitXBlockActions, courseId, navigate, refreshIframeContent],
[unitXBlockActions, courseId, navigate],
);
const handleDeleteXBlock = (usageId: string) => {
@@ -76,15 +75,10 @@ const XBlockContainerIframe: FC<XBlockContainerIframeProps> = ({
}
};
const handleRefetchXBlocks = useCallback(() => {
setTimeout(() => dispatch(fetchCourseUnitQuery(blockId)), 1000);
}, [dispatch, blockId]);
const onDeleteSubmit = () => {
if (deleteXBlockId) {
unitXBlockActions.handleDelete(deleteXBlockId);
closeDeleteModal();
refreshIframeContent();
}
};
@@ -92,19 +86,25 @@ const XBlockContainerIframe: FC<XBlockContainerIframeProps> = ({
if (configureXBlockId) {
handleConfigureSubmit(configureXBlockId, ...args, closeConfigureModal);
setAccessManagedXBlockData({});
refreshIframeContent();
}
};
const handleScrollToXBlock = (scrollOffset: number) => {
window.scrollBy({
top: scrollOffset,
behavior: 'smooth',
});
};
const messageHandlers = useMessageHandlers({
courseId,
navigate,
dispatch,
setIframeOffset,
handleDeleteXBlock,
handleRefetchXBlocks,
handleDuplicateXBlock,
handleManageXBlockAccess,
handleScrollToXBlock,
});
useIframeMessages(messageHandlers);

View File

@@ -7,7 +7,7 @@ const PasteButton = ({ onClick, text, className }) => {
const { blockId } = useParams();
const handlePasteXBlockComponent = () => {
onClick({ stagedContent: 'clipboard', parentLocator: blockId }, null, blockId);
onClick({ stagedContent: 'clipboard', parentLocator: blockId });
};
return (

View File

@@ -369,7 +369,7 @@ ConfigureModal.propTypes = {
supportsOnboarding: PropTypes.bool,
showReviewRules: PropTypes.bool,
onlineProctoringRules: PropTypes.string,
discussionEnabled: PropTypes.bool.isRequired,
discussionEnabled: PropTypes.bool,
}).isRequired,
isXBlockComponent: PropTypes.bool,
isSelfPaced: PropTypes.bool.isRequired,

View File

@@ -157,12 +157,15 @@ UnitTab.propTypes = {
isLibraryContent: PropTypes.bool,
values: PropTypes.shape({
isVisibleToStaffOnly: PropTypes.bool.isRequired,
discussionEnabled: PropTypes.bool.isRequired,
discussionEnabled: PropTypes.bool,
selectedPartitionIndex: PropTypes.oneOfType([
PropTypes.string,
PropTypes.number,
]).isRequired,
selectedGroups: PropTypes.arrayOf(PropTypes.string),
selectedGroups: PropTypes.oneOfType([
PropTypes.arrayOf(PropTypes.string),
PropTypes.array,
]),
}).isRequired,
setFieldValue: PropTypes.func.isRequired,
showWarning: PropTypes.bool.isRequired,