diff --git a/src/course-unit/CourseUnit.jsx b/src/course-unit/CourseUnit.jsx
index 10a9ad25b0..0054a77e24 100644
--- a/src/course-unit/CourseUnit.jsx
+++ b/src/course-unit/CourseUnit.jsx
@@ -41,6 +41,7 @@ const CourseUnit = ({ courseId }) => {
courseUnit,
isLoading,
sequenceId,
+ courseUnitLoadingStatus,
unitTitle,
unitCategory,
errorMessage,
@@ -210,6 +211,7 @@ const CourseUnit = ({ courseId }) => {
courseId={courseId}
blockId={blockId}
isUnitVerticalType={isUnitVerticalType}
+ courseUnitLoadingStatus={courseUnitLoadingStatus}
unitXBlockActions={unitXBlockActions}
courseVerticalChildren={courseVerticalChildren.children}
handleConfigureSubmit={handleConfigureSubmit}
diff --git a/src/course-unit/course-sequence/hooks.js b/src/course-unit/course-sequence/hooks.js
index 28035e1afd..cb541f1ab8 100644
--- a/src/course-unit/course-sequence/hooks.js
+++ b/src/course-unit/course-sequence/hooks.js
@@ -12,16 +12,19 @@ export function useSequenceNavigationMetadata(courseId, currentSequenceId, curre
const isLastUnit = !nextUrl;
const sequenceIds = useSelector(getSequenceIds);
const sequenceIndex = sequenceIds.indexOf(currentSequenceId);
- const unitIndex = sequence.unitIds.indexOf(currentUnitId);
+ let unitIndex = sequence?.unitIds.indexOf(currentUnitId);
const nextSequenceId = sequenceIndex < sequenceIds.length - 1 ? sequenceIds[sequenceIndex + 1] : null;
const previousSequenceId = sequenceIndex > 0 ? sequenceIds[sequenceIndex - 1] : null;
-
+ if (!unitIndex) {
+ // Handle case where unitIndex is not found
+ unitIndex = 0;
+ }
let nextLink;
const nextIndex = unitIndex + 1;
- if (nextIndex < sequence.unitIds.length) {
- const nextUnitId = sequence.unitIds[nextIndex];
+ if (nextIndex < sequence?.unitIds.length) {
+ const nextUnitId = sequence?.unitIds[nextIndex];
nextLink = `/course/${courseId}/container/${nextUnitId}/${currentSequenceId}`;
} else if (nextSequenceId) {
const pathToNextUnit = decodeURIComponent(nextUrl);
@@ -32,7 +35,7 @@ export function useSequenceNavigationMetadata(courseId, currentSequenceId, curre
const previousIndex = unitIndex - 1;
if (previousIndex >= 0) {
- const previousUnitId = sequence.unitIds[previousIndex];
+ const previousUnitId = sequence?.unitIds[previousIndex];
previousLink = `/course/${courseId}/container/${previousUnitId}/${currentSequenceId}`;
} else if (previousSequenceId) {
const pathToPreviousUnit = decodeURIComponent(prevUrl);
diff --git a/src/course-unit/course-sequence/sequence-navigation/SequenceNavigation.jsx b/src/course-unit/course-sequence/sequence-navigation/SequenceNavigation.jsx
index 0fa15fa29e..0af7ef63bf 100644
--- a/src/course-unit/course-sequence/sequence-navigation/SequenceNavigation.jsx
+++ b/src/course-unit/course-sequence/sequence-navigation/SequenceNavigation.jsx
@@ -35,7 +35,7 @@ const SequenceNavigation = ({
const shouldDisplayNotificationTriggerInSequence = useWindowSize().width < breakpoints.small.minWidth;
const renderUnitButtons = () => {
- if (sequence.unitIds?.length === 0 || unitId === null) {
+ if (sequence?.unitIds?.length === 0 || unitId === null) {
return (
);
@@ -43,7 +43,7 @@ const SequenceNavigation = ({
return (
state.courseUnit.courseVerti
export const getCourseOutlineInfo = (state) => state.courseUnit.courseOutlineInfo;
export const getCourseOutlineInfoLoadingStatus = (state) => state.courseUnit.courseOutlineInfoLoadingStatus;
export const getMovedXBlockParams = (state) => state.courseUnit.movedXBlockParams;
-const getLoadingStatuses = (state) => state.courseUnit.loadingStatus;
+export const getLoadingStatuses = (state) => state.courseUnit.loadingStatus;
export const getIsLoading = createSelector(
[getLoadingStatuses],
loadingStatus => Object.values(loadingStatus)
diff --git a/src/course-unit/hooks.jsx b/src/course-unit/hooks.jsx
index fc8fe092eb..f707bae452 100644
--- a/src/course-unit/hooks.jsx
+++ b/src/course-unit/hooks.jsx
@@ -35,6 +35,7 @@ import {
getSavingStatus,
getSequenceStatus,
getStaticFileNotices,
+ getLoadingStatuses,
} from './data/selectors';
import {
changeEditTitleFormOpen,
@@ -51,6 +52,7 @@ export const useCourseUnit = ({ courseId, blockId }) => {
const [isMoveModalOpen, openMoveModal, closeMoveModal] = useToggle(false);
const courseUnit = useSelector(getCourseUnitData);
+ const courseUnitLoadingStatus = useSelector(getLoadingStatuses);
const savingStatus = useSelector(getSavingStatus);
const isLoading = useSelector(getIsLoading);
const errorMessage = useSelector(getErrorMessage);
@@ -215,9 +217,28 @@ export const useCourseUnit = ({ courseId, blockId }) => {
}
}, [isMoveModalOpen]);
+ useEffect(() => {
+ const handlePageRefreshUsingStorage = (event) => {
+ // ignoring tests for if block, because it triggers when someone
+ // edits the component using editor which has a separate store
+ /* istanbul ignore next */
+ if (event.key === 'courseRefreshTriggerOnComponentEditSave') {
+ dispatch(fetchCourseSectionVerticalData(blockId, sequenceId));
+ dispatch(fetchCourseVerticalChildrenData(blockId, isSplitTestType));
+ localStorage.removeItem(event.key);
+ }
+ };
+
+ window.addEventListener('storage', handlePageRefreshUsingStorage);
+ return () => {
+ window.removeEventListener('storage', handlePageRefreshUsingStorage);
+ };
+ }, [blockId, sequenceId, isSplitTestType]);
+
return {
sequenceId,
courseUnit,
+ courseUnitLoadingStatus,
unitTitle,
unitCategory,
errorMessage,
diff --git a/src/course-unit/sidebar/utils.js b/src/course-unit/sidebar/utils.js
index af3263861f..390d9a3160 100644
--- a/src/course-unit/sidebar/utils.js
+++ b/src/course-unit/sidebar/utils.js
@@ -99,4 +99,4 @@ export const getIconVariant = (visibilityState, published, hasChanges) => {
* @param {string} id - The course unit ID.
* @returns {string} The clear course unit ID extracted from the provided data.
*/
-export const extractCourseUnitId = (id) => id.match(/block@(.+)$/)[1];
+export const extractCourseUnitId = (id) => id?.match(/block@(.+)$/)[1];
diff --git a/src/course-unit/xblock-container-iframe/index.tsx b/src/course-unit/xblock-container-iframe/index.tsx
index 48be568b27..ac6fc92933 100644
--- a/src/course-unit/xblock-container-iframe/index.tsx
+++ b/src/course-unit/xblock-container-iframe/index.tsx
@@ -37,9 +37,16 @@ import { useIframeContent } from '../../generic/hooks/useIframeContent';
import { useIframeMessages } from '../../generic/hooks/useIframeMessages';
import VideoSelectorPage from '../../editors/VideoSelectorPage';
import EditorPage from '../../editors/EditorPage';
+import { RequestStatus } from '../../data/constants';
const XBlockContainerIframe: FC = ({
- courseId, blockId, unitXBlockActions, courseVerticalChildren, handleConfigureSubmit, isUnitVerticalType,
+ courseId,
+ blockId,
+ unitXBlockActions,
+ courseVerticalChildren,
+ handleConfigureSubmit,
+ isUnitVerticalType,
+ courseUnitLoadingStatus,
}) => {
const intl = useIntl();
const dispatch = useDispatch();
@@ -70,6 +77,23 @@ const XBlockContainerIframe: FC = ({
setIframeRef(iframeRef);
}, [setIframeRef]);
+ useEffect(() => {
+ const iframe = iframeRef?.current;
+ if (!iframe) { return undefined; }
+
+ const handleIframeLoad = () => {
+ if (courseUnitLoadingStatus.fetchUnitLoadingStatus === RequestStatus.FAILED) {
+ window.location.reload();
+ }
+ };
+
+ iframe.addEventListener('load', handleIframeLoad);
+
+ return () => {
+ iframe.removeEventListener('load', handleIframeLoad);
+ };
+ }, [iframeRef]);
+
const onXBlockSave = useCallback(/* istanbul ignore next */ () => {
closeXBlockEditorModal();
closeVideoSelectorModal();
diff --git a/src/course-unit/xblock-container-iframe/types.ts b/src/course-unit/xblock-container-iframe/types.ts
index 084577d163..e83d5f759b 100644
--- a/src/course-unit/xblock-container-iframe/types.ts
+++ b/src/course-unit/xblock-container-iframe/types.ts
@@ -42,6 +42,11 @@ export interface XBlockContainerIframeProps {
courseId: string;
blockId: string;
isUnitVerticalType: boolean,
+ courseUnitLoadingStatus: {
+ fetchUnitLoadingStatus: string;
+ fetchVerticalChildrenLoadingStatus: string;
+ fetchXBlockDataLoadingStatus: string;
+ };
unitXBlockActions: {
handleDelete: (XBlockId: string | null) => void;
handleDuplicate: (XBlockId: string | null) => void;
diff --git a/src/editors/data/redux/thunkActions/app.js b/src/editors/data/redux/thunkActions/app.js
index 8da9f24b71..4c5079a5a9 100644
--- a/src/editors/data/redux/thunkActions/app.js
+++ b/src/editors/data/redux/thunkActions/app.js
@@ -125,6 +125,16 @@ export const saveBlock = (content, returnToUnit) => (dispatch) => {
content,
onSuccess: (response) => {
dispatch(actions.app.setSaveResponse(response));
+ const parsedData = JSON.parse(response.config.data);
+ if (parsedData?.has_changes) {
+ const storageKey = 'courseRefreshTriggerOnComponentEditSave';
+ localStorage.setItem(storageKey, Date.now());
+
+ window.dispatchEvent(new StorageEvent('storage', {
+ key: storageKey,
+ newValue: Date.now().toString(),
+ }));
+ }
returnToUnit(response.data);
},
}));
diff --git a/src/editors/data/redux/thunkActions/app.test.js b/src/editors/data/redux/thunkActions/app.test.js
index 35debc7f3c..3f8dc10c9e 100644
--- a/src/editors/data/redux/thunkActions/app.test.js
+++ b/src/editors/data/redux/thunkActions/app.test.js
@@ -352,7 +352,11 @@ describe('app thunkActions', () => {
});
it('dispatches actions.app.setSaveResponse with response and then calls returnToUnit', () => {
dispatch.mockClear();
- const response = 'testRESPONSE';
+ const mockParsedData = { has_changes: true };
+ const response = {
+ config: { data: JSON.stringify(mockParsedData) },
+ data: {},
+ };
calls[1][0].saveBlock.onSuccess(response);
expect(dispatch).toHaveBeenCalledWith(actions.app.setSaveResponse(response));
expect(returnToUnit).toHaveBeenCalled();
diff --git a/src/files-and-videos/files-page/FilesPage.test.jsx b/src/files-and-videos/files-page/FilesPage.test.jsx
index c1aad27252..c554646567 100644
--- a/src/files-and-videos/files-page/FilesPage.test.jsx
+++ b/src/files-and-videos/files-page/FilesPage.test.jsx
@@ -70,15 +70,6 @@ const mockStore = async (
}
renderComponent();
await executeThunk(fetchAssets(courseId), store.dispatch);
-
- // Finish loading the expected files into the data table before returning,
- // because loading new files can disrupt things like accessing file menus.
- if (status === RequestStatus.SUCCESSFUL) {
- const numFiles = skipNextPageFetch ? 13 : 15;
- await waitFor(() => {
- expect(screen.getByText(`Showing ${numFiles} of ${numFiles}`)).toBeInTheDocument();
- });
- }
};
const emptyMockStore = async (status) => {
diff --git a/src/files-and-videos/files-page/data/slice.js b/src/files-and-videos/files-page/data/slice.js
index 3a96779185..4fbe4915c9 100644
--- a/src/files-and-videos/files-page/data/slice.js
+++ b/src/files-and-videos/files-page/data/slice.js
@@ -28,7 +28,7 @@ const slice = createSlice({
if (isEmpty(state.assetIds)) {
state.assetIds = payload.assetIds;
} else {
- state.assetIds = [...state.assetIds, ...payload.assetIds];
+ state.assetIds = [...new Set([...state.assetIds, ...payload.assetIds])];
}
},
setSortedAssetIds: (state, { payload }) => {
diff --git a/src/files-and-videos/generic/DeleteConfirmationModal.jsx b/src/files-and-videos/generic/DeleteConfirmationModal.jsx
index 0955c83a5c..b6f4627855 100644
--- a/src/files-and-videos/generic/DeleteConfirmationModal.jsx
+++ b/src/files-and-videos/generic/DeleteConfirmationModal.jsx
@@ -1,4 +1,4 @@
-import React from 'react';
+import React, { useContext } from 'react';
import PropTypes from 'prop-types';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { getConfig } from '@edx/frontend-platform';
@@ -7,6 +7,7 @@ import {
AlertModal,
Button,
Collapsible,
+ DataTableContext,
Hyperlink,
Truncate,
} from '@openedx/paragon';
@@ -22,6 +23,13 @@ const DeleteConfirmationModal = ({
// injected
intl,
}) => {
+ const { clearSelection } = useContext(DataTableContext);
+
+ const handleConfirmDeletion = () => {
+ handleBulkDelete();
+ clearSelection();
+ };
+
const firstSelectedRow = selectedRows[0]?.original;
let activeContentRows = [];
if (Array.isArray(selectedRows)) {
@@ -73,7 +81,7 @@ const DeleteConfirmationModal = ({
-