Skip to content

fix: prevent full iframe reload after xblock edition #2257

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 6 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/course-unit/CourseUnit.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ const CourseUnit = ({ courseId }) => {
handleCloseXBlockMovedAlert,
handleNavigateToTargetUnit,
addComponentTemplateData,
resetXBlockPublishState,
} = useCourseUnit({ courseId, blockId });
const layoutGrid = useLayoutGrid(unitCategory, isUnitLibraryType);

Expand Down Expand Up @@ -213,6 +214,7 @@ const CourseUnit = ({ courseId }) => {
unitXBlockActions={unitXBlockActions}
courseVerticalChildren={courseVerticalChildren.children}
handleConfigureSubmit={handleConfigureSubmit}
resetXBlockPublishState={resetXBlockPublishState}
/>
{!readOnly && (
<AddComponent
Expand Down
55 changes: 55 additions & 0 deletions src/course-unit/CourseUnit.test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -2304,4 +2304,59 @@ describe('<CourseUnit />', () => {
// Does not render the "Add Components" section
expect(screen.queryByText(addComponentMessages.title.defaultMessage)).not.toBeInTheDocument();
});

it('resets XBlock publish state after discarding changes', async () => {
render(<RootWrapper />);
let courseUnitSidebar;
let discardChangesBtn;

await waitFor(() => {
courseUnitSidebar = screen.getByTestId('course-unit-sidebar');
// Ensure we are in the draft/unpublished state
expect(within(courseUnitSidebar)
.getByText(sidebarMessages.sidebarTitleDraftUnpublishedChanges.defaultMessage)).toBeInTheDocument();
discardChangesBtn = within(courseUnitSidebar)
.queryByRole('button', { name: sidebarMessages.actionButtonDiscardChangesTitle.defaultMessage });
expect(discardChangesBtn).toBeInTheDocument();
userEvent.click(discardChangesBtn);
});

// Confirm discard in modal
const modalNotification = await screen.findByRole('dialog');
const actionBtn = within(modalNotification)
.getByRole('button', { name: sidebarMessages.modalDiscardUnitChangesActionButtonText.defaultMessage });
userEvent.click(actionBtn);

// Mock API responses for discard
axiosMock
.onPost(getXBlockBaseApiUrl(blockId), {
publish: PUBLISH_TYPES.discardChanges,
})
.reply(200, { dummy: 'value' });
axiosMock
.onGet(getCourseSectionVerticalApiUrl(blockId))
.reply(200, {
...courseSectionVerticalMock,
xblock_info: {
...courseSectionVerticalMock.xblock_info,
published: true,
has_changes: false,
},
});

await executeThunk(editCourseUnitVisibilityAndData(
blockId,
PUBLISH_TYPES.discardChanges,
true,
), store.dispatch);

// Now the sidebar should reflect the published state (no draft/unpublished changes)
await waitFor(() => {
expect(within(courseUnitSidebar)
.getByText(sidebarMessages.sidebarTitlePublishedNotYetReleased.defaultMessage)).toBeInTheDocument();
expect(
within(courseUnitSidebar).queryByRole('button', { name: sidebarMessages.actionButtonDiscardChangesTitle.defaultMessage }),
).not.toBeInTheDocument();
});
});
});
5 changes: 5 additions & 0 deletions src/course-unit/data/slice.js
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,10 @@ const slice = createSlice({
updateMovedXBlockParams: (state, { payload }) => {
state.movedXBlockParams = { ...state.movedXBlockParams, ...payload };
},
setXBlockPublishState: (state, { payload }) => {
state.courseSectionVertical.xblockInfo.published = payload;
state.courseSectionVertical.xblockInfo.hasChanges = !payload;
},
},
});

Expand All @@ -108,6 +112,7 @@ export const {
updateCourseOutlineInfo,
updateCourseOutlineInfoLoadingStatus,
updateMovedXBlockParams,
setXBlockPublishState,
} = slice.actions;

export const {
Expand Down
31 changes: 31 additions & 0 deletions src/course-unit/data/slice.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { reducer, setXBlockPublishState } from './slice';

describe('setXBlockPublishState reducer', () => {
it('sets published and hasChanges correctly when payload is true', () => {
const prevState = {
courseSectionVertical: {
xblockInfo: {
published: false,
hasChanges: true,
},
},
};
const nextState = reducer(prevState, setXBlockPublishState(true));
expect(nextState.courseSectionVertical.xblockInfo.published).toBe(true);
expect(nextState.courseSectionVertical.xblockInfo.hasChanges).toBe(false);
});

it('sets published and hasChanges correctly when payload is false', () => {
const prevState = {
courseSectionVertical: {
xblockInfo: {
published: true,
hasChanges: false,
},
},
};
const nextState = reducer(prevState, setXBlockPublishState(false));
expect(nextState.courseSectionVertical.xblockInfo.published).toBe(false);
expect(nextState.courseSectionVertical.xblockInfo.hasChanges).toBe(true);
});
});
24 changes: 6 additions & 18 deletions src/course-unit/hooks.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ import {
} from './data/selectors';
import {
changeEditTitleFormOpen,
setXBlockPublishState,
updateMovedXBlockParams,
updateQueryPendingStatus,
} from './data/slice';
Expand Down Expand Up @@ -163,6 +164,10 @@ export const useCourseUnit = ({ courseId, blockId }) => {
dispatch(updateMovedXBlockParams({ isSuccess: false }));
};

const resetXBlockPublishState = () => {
dispatch(setXBlockPublishState(false));
};

const handleNavigateToTargetUnit = () => {
navigate(`/course/${courseId}/container/${movedXBlockParams.targetParentLocator}`);
};
Expand Down Expand Up @@ -213,24 +218,6 @@ 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,
Expand Down Expand Up @@ -266,6 +253,7 @@ export const useCourseUnit = ({ courseId, blockId }) => {
handleNavigateToTargetUnit,
addComponentTemplateData,
setAddComponentTemplateData,
resetXBlockPublishState,
};
};

Expand Down
14 changes: 11 additions & 3 deletions src/course-unit/xblock-container-iframe/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,13 @@ import VideoSelectorPage from '../../editors/VideoSelectorPage';
import EditorPage from '../../editors/EditorPage';

const XBlockContainerIframe: FC<XBlockContainerIframeProps> = ({
courseId, blockId, unitXBlockActions, courseVerticalChildren, handleConfigureSubmit, isUnitVerticalType,
courseId,
blockId,
unitXBlockActions,
courseVerticalChildren,
handleConfigureSubmit,
isUnitVerticalType,
resetXBlockPublishState,
}) => {
const intl = useIntl();
const dispatch = useDispatch();
Expand Down Expand Up @@ -73,8 +79,10 @@ const XBlockContainerIframe: FC<XBlockContainerIframeProps> = ({
const onXBlockSave = useCallback(/* istanbul ignore next */ () => {
closeXBlockEditorModal();
closeVideoSelectorModal();
sendMessageToIframe(messageTypes.refreshXBlock, null);
}, [closeXBlockEditorModal, closeVideoSelectorModal, sendMessageToIframe]);
sendMessageToIframe(messageTypes.completeXBlockEditing, { locator: newBlockId });
// This ensures the publish button is able
resetXBlockPublishState();
}, [closeXBlockEditorModal, closeVideoSelectorModal, sendMessageToIframe, newBlockId]);

const handleEditXBlock = useCallback((type: string, id: string) => {
setBlockType(type);
Expand Down
1 change: 1 addition & 0 deletions src/course-unit/xblock-container-iframe/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ export interface XBlockContainerIframeProps {
};
courseVerticalChildren: Array<XBlockTypes>;
handleConfigureSubmit: (XBlockId: string, ...args: any[]) => void;
resetXBlockPublishState: () => void;
}

export type UserPartitionInfoTypes = {
Expand Down
6 changes: 4 additions & 2 deletions src/editors/containers/EditorContainer/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -78,8 +78,10 @@ const EditorContainer: React.FC<Props> = ({
});

const onSave = () => {
setSaved(true);
handleSave();
if (isDirty()) {
setSaved(true);
handleSave();
}
};
// Stops user from navigating away if they have unsaved changes.
usePromptIfDirty(() => {
Expand Down
10 changes: 0 additions & 10 deletions src/editors/data/redux/thunkActions/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -125,16 +125,6 @@ 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);
},
}));
Expand Down
6 changes: 1 addition & 5 deletions src/editors/data/redux/thunkActions/app.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -352,11 +352,7 @@ describe('app thunkActions', () => {
});
it('dispatches actions.app.setSaveResponse with response and then calls returnToUnit', () => {
dispatch.mockClear();
const mockParsedData = { has_changes: true };
const response = {
config: { data: JSON.stringify(mockParsedData) },
data: {},
};
const response = 'testRESPONSE';
calls[1][0].saveBlock.onSuccess(response);
expect(dispatch).toHaveBeenCalledWith(actions.app.setSaveResponse(response));
expect(returnToUnit).toHaveBeenCalled();
Expand Down
Loading