From 5b2b33aa8c98722388965a4da1c298f46aef0bd0 Mon Sep 17 00:00:00 2001 From: Glib Glugovskiy Date: Mon, 29 Apr 2024 12:44:06 +0300 Subject: [PATCH 1/3] feat: added move modal and tests feat: added alert for successful unit movement feat: added alert logic refactor: code refactoring refactor: some refactoring refactor: code refactoring refactor: code refactoring --- src/constants.js | 1 + src/course-unit/CourseUnit.jsx | 35 +++- src/course-unit/CourseUnit.test.jsx | 170 +++++++++++++++++- .../course-xblock/CourseXBlock.jsx | 58 ++++-- src/course-unit/course-xblock/utils.js | 12 +- src/course-unit/data/api.js | 16 ++ src/course-unit/data/selectors.js | 1 + src/course-unit/data/slice.js | 11 ++ src/course-unit/data/thunk.js | 23 +++ src/course-unit/hooks.jsx | 26 ++- src/course-unit/messages.js | 24 +++ 11 files changed, 357 insertions(+), 20 deletions(-) diff --git a/src/constants.js b/src/constants.js index a641c8add8..8cc99b58d4 100644 --- a/src/constants.js +++ b/src/constants.js @@ -27,6 +27,7 @@ export const NOTIFICATION_MESSAGES = { copying: 'Copying', pasting: 'Pasting', discardChanges: 'Discarding changes', + undoMoving: 'Undo moving', publishing: 'Publishing', hidingFromStudents: 'Hiding from students', makingVisibleToStudents: 'Making visible to students', diff --git a/src/course-unit/CourseUnit.jsx b/src/course-unit/CourseUnit.jsx index 78421e078e..592865c19f 100644 --- a/src/course-unit/CourseUnit.jsx +++ b/src/course-unit/CourseUnit.jsx @@ -3,13 +3,14 @@ import PropTypes from 'prop-types'; import { useSelector } from 'react-redux'; import { useParams } from 'react-router-dom'; import { - Container, Layout, Stack, Button, + Container, Layout, Stack, Button, TransitionReplace, } from '@openedx/paragon'; import { useIntl, injectIntl } from '@edx/frontend-platform/i18n'; import { Warning as WarningIcon, ArrowDropDown as ArrowDownIcon, ArrowDropUp as ArrowUpIcon, + CheckCircle as CheckCircleIcon, } from '@openedx/paragon/icons'; import { SortableContext, verticalListSortingStrategy } from '@dnd-kit/sortable'; import { DraggableList } from '@edx/frontend-lib-content-components'; @@ -67,6 +68,10 @@ const CourseUnit = ({ courseId }) => { isXBlocksExpanded, isXBlocksRendered, handleExpandAll, + movedXBlockParams, + handleRollbackMovedXBlock, + handleCloseXBlockMovedAlert, + handleNavigateToTargetUnit, } = useCourseUnit({ courseId, blockId }); const initialXBlocksData = useMemo(() => courseVerticalChildren.children ?? [], [courseVerticalChildren.children]); @@ -111,6 +116,34 @@ const CourseUnit = ({ courseId }) => { <>
+ + {movedXBlockParams.isSuccess ? ( + + {intl.formatMessage(messages.undoMoveButton)} + , + , + ]} + onClose={handleCloseXBlockMovedAlert} + /> + ) : null} + ({ global.BroadcastChannel = jest.fn(() => clipboardBroadcastChannelMock); +const getIFramePostMessages = (method) => ({ + data: { + method, + params: { + targetParentLocator: courseId, + sourceDisplayName: courseVerticalChildrenMock.children[0].name, + sourceLocator: courseVerticalChildrenMock.children[0].block_id, + }, + }, +}); + const RootWrapper = () => ( @@ -1590,4 +1599,153 @@ describe('', () => { }); }); }); + + describe('Edit and move modals', () => { + it('should close the edit modal when the close button is clicked', async () => { + const { getByTitle, getAllByTestId } = render(); + + axiosMock + .onGet(getCourseVerticalChildrenApiUrl(blockId)) + .reply(200, courseVerticalChildrenMock); + + await executeThunk(fetchCourseVerticalChildrenData(blockId), store.dispatch); + + const [discussionXBlock] = getAllByTestId('course-xblock'); + const xblockEditBtn = within(discussionXBlock) + .getByLabelText(courseXBlockMessages.blockAltButtonEdit.defaultMessage); + + userEvent.click(xblockEditBtn); + + const iframePostMsg = getIFramePostMessages('close_edit_modal'); + const editModalIFrame = getByTitle('xblock-edit-modal-iframe'); + + expect(editModalIFrame).toHaveAttribute('src', `${getConfig().STUDIO_BASE_URL}/xblock/${courseVerticalChildrenMock.children[0].block_id}/actions/edit`); + + await act(async () => window.dispatchEvent(new MessageEvent('message', iframePostMsg))); + + expect(editModalIFrame).not.toBeInTheDocument(); + }); + + it('should display success alert and close move modal when move event is triggered', async () => { + const { + getByTitle, + getByRole, + getAllByLabelText, + getByText, + } = render(); + + const iframePostMsg = getIFramePostMessages('move_xblock'); + + axiosMock + .onGet(getCourseVerticalChildrenApiUrl(blockId)) + .reply(200, courseVerticalChildrenMock); + + await executeThunk(fetchCourseVerticalChildrenData(blockId), store.dispatch); + + const [xblockActionBtn] = getAllByLabelText(courseXBlockMessages.blockActionsDropdownAlt.defaultMessage); + userEvent.click(xblockActionBtn); + + const xblockMoveBtn = getByRole('button', { name: courseXBlockMessages.blockLabelButtonMove.defaultMessage }); + userEvent.click(xblockMoveBtn); + + const moveModalIFrame = getByTitle('xblock-move-modal-iframe'); + + await act(async () => window.dispatchEvent(new MessageEvent('message', iframePostMsg))); + + expect(moveModalIFrame).not.toBeInTheDocument(); + expect(getByText(messages.alertMoveSuccessTitle.defaultMessage)).toBeInTheDocument(); + expect(getByText( + messages.alertMoveSuccessDescription.defaultMessage + .replace('{title}', courseVerticalChildrenMock.children[0].name), + )).toBeInTheDocument(); + + await waitFor(() => { + userEvent.click(getByText(/Cancel/i)); + expect(moveModalIFrame).not.toBeInTheDocument(); + }); + }); + + it('should navigate to new location when new location button is clicked after successful move', async () => { + const { + getByTitle, + getByRole, + getAllByLabelText, + getByText, + } = render(); + + const iframePostMsg = getIFramePostMessages('move_xblock'); + + axiosMock + .onGet(getCourseVerticalChildrenApiUrl(blockId)) + .reply(200, courseVerticalChildrenMock); + + await executeThunk(fetchCourseVerticalChildrenData(blockId), store.dispatch); + + const [xblockActionBtn] = getAllByLabelText(courseXBlockMessages.blockActionsDropdownAlt.defaultMessage); + userEvent.click(xblockActionBtn); + + const xblockMoveBtn = getByRole('button', { name: courseXBlockMessages.blockLabelButtonMove.defaultMessage }); + userEvent.click(xblockMoveBtn); + + const moveModalIFrame = getByTitle('xblock-move-modal-iframe'); + + await act(async () => window.dispatchEvent(new MessageEvent('message', iframePostMsg))); + + expect(moveModalIFrame).not.toBeInTheDocument(); + expect(getByText(messages.alertMoveSuccessTitle.defaultMessage)).toBeInTheDocument(); + expect(getByText( + messages.alertMoveSuccessDescription.defaultMessage + .replace('{title}', courseVerticalChildrenMock.children[0].name), + )).toBeInTheDocument(); + + await waitFor(() => { + userEvent.click(getByText(messages.newLocationButton.defaultMessage)); + expect(mockedUsedNavigate).toHaveBeenCalledWith(`/course/${courseId}/container/${iframePostMsg.data.params.targetParentLocator}`); + }); + }); + + it('should display move cancellation alert when undo move button is clicked', async () => { + const { + getByRole, + getAllByLabelText, + getByText, + } = render(); + + const iframePostMsg = getIFramePostMessages('move_xblock'); + + axiosMock + .onGet(getCourseVerticalChildrenApiUrl(blockId)) + .reply(200, courseVerticalChildrenMock); + + await executeThunk(fetchCourseVerticalChildrenData(blockId), store.dispatch); + + const [xblockActionBtn] = getAllByLabelText(courseXBlockMessages.blockActionsDropdownAlt.defaultMessage); + userEvent.click(xblockActionBtn); + + const xblockMoveBtn = getByRole('button', { name: courseXBlockMessages.blockLabelButtonMove.defaultMessage }); + userEvent.click(xblockMoveBtn); + + await act(async () => window.dispatchEvent(new MessageEvent('message', iframePostMsg))); + + await waitFor(() => userEvent.click(getByText(messages.undoMoveButton.defaultMessage))); + + axiosMock + .onPatch(postXBlockBaseApiUrl(), { + parent_locator: blockId, + move_source_locator: courseVerticalChildrenMock.children[0].block_id, + }) + .reply(200, { + parent_locator: blockId, + move_source_locator: courseVerticalChildrenMock.children[0].block_id, + }); + + await executeThunk(rollbackUnitItemQuery(blockId, courseVerticalChildrenMock.children[0].block_id, 'Discussion'), store.dispatch); + + expect(getByText(messages.alertMoveCancelTitle.defaultMessage)).toBeInTheDocument(); + expect(getByText( + messages.alertMoveCancelDescription.defaultMessage + .replace('{title}', courseVerticalChildrenMock.children[0].name), + )).toBeInTheDocument(); + }); + }); }); diff --git a/src/course-unit/course-xblock/CourseXBlock.jsx b/src/course-unit/course-xblock/CourseXBlock.jsx index f1e2698dd0..003c7ed870 100644 --- a/src/course-unit/course-xblock/CourseXBlock.jsx +++ b/src/course-unit/course-xblock/CourseXBlock.jsx @@ -15,7 +15,6 @@ import { import { useIntl } from '@edx/frontend-platform/i18n'; import { useNavigate, useSearchParams } from 'react-router-dom'; import { find } from 'lodash'; -import { getConfig } from '@edx/frontend-platform'; import classNames from 'classnames'; import { useOverflowControl } from '../../generic/hooks'; @@ -38,15 +37,16 @@ import { fetchCourseVerticalChildrenData, fetchXBlockIFrameHtmlAndResourcesQuery, } from '../data/thunk'; +import { updateMovedXBlockParams } from '../data/slice'; import { COMPONENT_TYPES } from '../constants'; import XBlockMessages from './xblock-messages/XBlockMessages'; import RenderErrorAlert from './render-error-alert'; import { XBlockContent } from './xblock-content'; import messages from './messages'; -import { extractStylesWithContent } from './utils'; +import { extractStylesWithContent, getXBlockActionsBasePath } from './utils'; import CourseIFrame from './CourseIFrame'; -const XBLOCK_EDIT_MODAL_CLASS_NAME = 'xblock-edit-modal'; +const XBLOCK_LEGACY_MODAL_CLASS_NAME = 'xblock-edit-modal'; const CourseXBlock = memo(({ id, title, type, unitXBlockActions, shouldScroll, userPartitionInfo, @@ -67,7 +67,8 @@ const CourseXBlock = memo(({ [id, xblockIFrameHtmlAndResources], ); const [showLegacyEditModal, toggleLegacyEditModal] = useState(false); - const xblockLegacyEditModalRef = useRef(null); + const [showLegacyMoveModal, toggleLegacyMoveModal] = useState(false); + const xblockLegacyModalRef = useRef(null); const [isExpanded, setIsExpanded] = useState(isXBlocksExpanded); const [isRendered, setIsRendered] = useState(isXBlocksRendered); @@ -80,17 +81,31 @@ const CourseXBlock = memo(({ canCopy, canDelete, canDuplicate, canManageAccess, canManageTags, canMove, } = actions; - useOverflowControl(`.${XBLOCK_EDIT_MODAL_CLASS_NAME}`); + useOverflowControl(`.${XBLOCK_LEGACY_MODAL_CLASS_NAME}`); useEffect(() => { const handleMessage = (event) => { - const { method } = event.data; + const { method, params } = event.data; if (method === 'close_edit_modal') { toggleLegacyEditModal(false); dispatch(fetchCourseVerticalChildrenData(blockId)); dispatch(fetchXBlockIFrameHtmlAndResourcesQuery(id)); dispatch(fetchCourseUnitQuery(blockId)); + } else if (method === 'close_move_modal') { + toggleLegacyMoveModal(false); + dispatch(fetchCourseVerticalChildrenData(blockId)); + dispatch(fetchXBlockIFrameHtmlAndResourcesQuery(id)); + dispatch(fetchCourseUnitQuery(blockId)); + } else if (method === 'move_xblock') { + toggleLegacyMoveModal(false); + dispatch(updateMovedXBlockParams({ + title: params.sourceDisplayName, + isSuccess: true, + sourceLocator: params.sourceLocator, + targetParentLocator: params.targetParentLocator, + })); + window.scrollTo({ top: 0, behavior: 'smooth' }); } }; @@ -99,7 +114,7 @@ const CourseXBlock = memo(({ return () => { window.removeEventListener('message', handleMessage); }; - }, [xblockLegacyEditModalRef]); + }, [xblockLegacyModalRef]); const { data: contentTaxonomyTagsCount, @@ -151,6 +166,17 @@ const CourseXBlock = memo(({ } }; + const handleXBlockMove = () => { + toggleLegacyMoveModal(true); + dispatch(updateMovedXBlockParams({ + isSuccess: false, + isUndo: false, + title: '', + sourceLocator: '', + targetParentLocator: '', + })); + }; + const onConfigureSubmit = (...arg) => { handleConfigureSubmit(id, ...arg, closeConfigureModal); }; @@ -170,12 +196,22 @@ const CourseXBlock = memo(({ return ( <> {showLegacyEditModal && ( -
+
+
+ )} + {showLegacyMoveModal && ( +
+
)} @@ -247,7 +283,7 @@ const CourseXBlock = memo(({ )} {canMove && ( - + {intl.formatMessage(messages.blockLabelButtonMove)} )} diff --git a/src/course-unit/course-xblock/utils.js b/src/course-unit/course-xblock/utils.js index 7d0617daee..b299e41713 100644 --- a/src/course-unit/course-xblock/utils.js +++ b/src/course-unit/course-xblock/utils.js @@ -1,3 +1,5 @@ +import { getConfig } from '@edx/frontend-platform'; + import { STYLE_TAG_PATTERN } from './constants'; /** @@ -5,7 +7,6 @@ import { STYLE_TAG_PATTERN } from './constants'; * @param {string} htmlString - The HTML string to extract styles from. * @returns {string[]} An array containing the content of