Skip to content

Commit

Permalink
feat: [FC-0070] implement move xblock modal
Browse files Browse the repository at this point in the history
  • Loading branch information
ihor-romaniuk committed Nov 8, 2024
1 parent e59f284 commit de9f97c
Show file tree
Hide file tree
Showing 28 changed files with 3,385 additions and 13 deletions.
4 changes: 2 additions & 2 deletions src/CourseAuthoringRoutes.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import ScheduleAndDetails from './schedule-and-details';
import { GradingSettings } from './grading-settings';
import CourseTeam from './course-team/CourseTeam';
import { CourseUpdates } from './course-updates';
import { CourseUnit } from './course-unit';
import { CourseUnit, IframeProvider } from './course-unit';
import { Certificates } from './certificates';
import CourseExportPage from './export-page/CourseExportPage';
import CourseImportPage from './import-page/CourseImportPage';
Expand Down Expand Up @@ -79,7 +79,7 @@ const CourseAuthoringRoutes = () => {
<Route
key={path}
path={path}
element={<PageWrap><CourseUnit courseId={courseId} /></PageWrap>}
element={<PageWrap><IframeProvider><CourseUnit courseId={courseId} /></IframeProvider></PageWrap>}
/>
))}
<Route
Expand Down
2 changes: 2 additions & 0 deletions src/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ export const NOTIFICATION_MESSAGES = {
copying: 'Copying',
pasting: 'Pasting',
discardChanges: 'Discarding changes',
moving: 'Moving',
undoMoving: 'Undo moving',
publishing: 'Publishing',
hidingFromStudents: 'Hiding from students',
makingVisibleToStudents: 'Making visible to students',
Expand Down
57 changes: 55 additions & 2 deletions src/course-unit/CourseUnit.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,15 @@ import { useEffect } from 'react';
import PropTypes from 'prop-types';
import { useSelector } from 'react-redux';
import { useParams } from 'react-router-dom';
import { Container, Layout, Stack } from '@openedx/paragon';
import {
Container, Layout, Stack, Button, TransitionReplace,
} from '@openedx/paragon';
import { getConfig } from '@edx/frontend-platform';
import { useIntl, injectIntl } from '@edx/frontend-platform/i18n';
import { Warning as WarningIcon } from '@openedx/paragon/icons';
import {
Warning as WarningIcon,
CheckCircle as CheckCircleIcon,
} from '@openedx/paragon/icons';

import { getProcessingNotification } from '../generic/processing-notification/data/selectors';
import SubHeader from '../generic/sub-header/SubHeader';
Expand All @@ -30,6 +35,7 @@ import LocationInfo from './sidebar/LocationInfo';
import TagsSidebarControls from '../content-tags-drawer/tags-sidebar-controls';
import { PasteNotificationAlert } from './clipboard';
import XBlockContainerIframe from './xblock-container-iframe';
import MoveModal from './move-modal';

const CourseUnit = ({ courseId }) => {
const { blockId } = useParams();
Expand All @@ -55,6 +61,13 @@ const CourseUnit = ({ courseId }) => {
handleConfigureSubmit,
courseVerticalChildren,
canPasteComponent,
isMoveModalOpen,
openMoveModal,
closeMoveModal,
movedXBlockParams,
handleRollbackMovedXBlock,
handleCloseXBlockMovedAlert,
handleNavigateToTargetUnit,
} = useCourseUnit({ courseId, blockId });

useEffect(() => {
Expand Down Expand Up @@ -82,6 +95,40 @@ const CourseUnit = ({ courseId }) => {
<>
<Container size="xl" className="course-unit px-4">
<section className="course-unit-container mb-4 mt-5">
<TransitionReplace>
{movedXBlockParams.isSuccess ? (
<AlertMessage
key="xblock-moved-alert"
data-testid="xblock-moved-alert"
show={movedXBlockParams.isSuccess}
variant="success"
icon={CheckCircleIcon}
title={movedXBlockParams.isUndo
? intl.formatMessage(messages.alertMoveCancelTitle)
: intl.formatMessage(messages.alertMoveSuccessTitle)}
description={movedXBlockParams.isUndo
? intl.formatMessage(messages.alertMoveCancelDescription, { title: movedXBlockParams.title })
: intl.formatMessage(messages.alertMoveSuccessDescription, { title: movedXBlockParams.title })}
aria-hidden={movedXBlockParams.isSuccess}
dismissible
actions={movedXBlockParams.isUndo ? null : [
<Button
onClick={handleRollbackMovedXBlock}
key="xblock-moved-alert-undo-move-button"
>
{intl.formatMessage(messages.undoMoveButton)}
</Button>,
<Button
onClick={handleNavigateToTargetUnit}
key="xblock-moved-alert-new-location-button"
>
{intl.formatMessage(messages.newLocationButton)}
</Button>,
]}
onClose={handleCloseXBlockMovedAlert}
/>
) : null}
</TransitionReplace>
<SubHeader
hideBorder
title={(
Expand Down Expand Up @@ -147,6 +194,12 @@ const CourseUnit = ({ courseId }) => {
text={intl.formatMessage(messages.pasteButtonText)}
/>
)}
<MoveModal
isOpenModal={isMoveModalOpen}
openModal={openMoveModal}
closeModal={closeMoveModal}
courseId={courseId}
/>
</Layout.Element>
<Layout.Element>
<Stack gap={3}>
Expand Down
1 change: 1 addition & 0 deletions src/course-unit/CourseUnit.scss
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
@import "./add-component/AddComponent";
@import "./sidebar/Sidebar";
@import "./header-title/HeaderTitle";
@import "./move-modal";

.course-unit__alert {
margin-bottom: 1.75rem;
Expand Down
186 changes: 183 additions & 3 deletions src/course-unit/CourseUnit.test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import {
getCourseSectionVerticalApiUrl,
getCourseUnitApiUrl,
getCourseVerticalChildrenApiUrl,
getOutlineInfo,
getXBlockBaseApiUrl,
postXBlockBaseApiUrl,
} from './data/api';
Expand All @@ -27,6 +28,8 @@ import {
fetchCourseSectionVerticalData,
fetchCourseUnitQuery,
fetchCourseVerticalChildrenData,
getCourseOutlineInfoQuery,
patchUnitItemQuery,
} from './data/thunk';
import initializeStore from '../store';
import {
Expand All @@ -36,6 +39,7 @@ import {
courseUnitMock,
courseVerticalChildrenMock,
clipboardMockResponse,
courseOutlineInfoMock,
} from './__mocks__';
import { clipboardUnit } from '../__mocks__';
import { executeThunk } from '../utils';
Expand All @@ -49,10 +53,12 @@ import { extractCourseUnitId } from './sidebar/utils';
import CourseUnit from './CourseUnit';

import configureModalMessages from '../generic/configure-modal/messages';
import { getContentTaxonomyTagsApiUrl, getContentTaxonomyTagsCountApiUrl } from '../content-tags-drawer/data/api';
import addComponentMessages from './add-component/messages';
import { PUBLISH_TYPES, UNIT_VISIBILITY_STATES } from './constants';
import { messageTypes, PUBLISH_TYPES, UNIT_VISIBILITY_STATES } from './constants';
import { IframeProvider } from './context/iFrameContext';
import moveModalMessages from './move-modal/messages';
import messages from './messages';
import { getContentTaxonomyTagsApiUrl, getContentTaxonomyTagsCountApiUrl } from '../content-tags-drawer/data/api';

let axiosMock;
let store;
Expand Down Expand Up @@ -108,7 +114,9 @@ global.BroadcastChannel = jest.fn(() => clipboardBroadcastChannelMock);
const RootWrapper = () => (
<AppProvider store={store}>
<IntlProvider locale="en">
<CourseUnit courseId={courseId} />
<IframeProvider>
<CourseUnit courseId={courseId} />
</IframeProvider>
</IntlProvider>
</AppProvider>
);
Expand All @@ -123,6 +131,7 @@ describe('<CourseUnit />', () => {
roles: [],
},
});
window.scrollTo = jest.fn();
global.localStorage.clear();
store = initializeStore();
axiosMock = new MockAdapter(getAuthenticatedHttpClient());
Expand Down Expand Up @@ -1061,4 +1070,175 @@ describe('<CourseUnit />', () => {
)).not.toBeInTheDocument();
});
});

describe('Move functionality', () => {
const requestData = {
sourceLocator: 'block-v1:edX+DemoX+Demo_Course+type@vertical+block@867dddb6f55d410caaa9c1eb9c6743ec',
targetParentLocator: 'block-v1:edX+DemoX+Demo_Course+type@course+block@course',
title: 'Getting Started',
currentParentLocator: 'block-v1:edX+DemoX+Demo_Course+type@sequential+block@19a30717eff543078a5d94ae9d6c18a5',
isMoving: true,
callbackFn: jest.fn(),
};
const messageEvent = new MessageEvent('message', {
data: {
type: messageTypes.showMoveXBlockModal,
payload: {
sourceXBlockInfo: {
id: requestData.sourceLocator,
displayName: requestData.title,
},
sourceParentXBlockInfo: {
id: requestData.currentParentLocator,
category: 'vertical',
hasChildren: true,
},
},
},
origin: '*',
});

it('should display "Move Modal" on receive trigger message', async () => {
const {
getByText,
getByRole,
} = render(<RootWrapper />);

await waitFor(() => {
expect(getByText(unitDisplayName)).toBeInTheDocument();
});

axiosMock
.onGet(getOutlineInfo(courseId))
.reply(200, courseOutlineInfoMock);
await executeThunk(getCourseOutlineInfoQuery(courseId), store.dispatch);

window.dispatchEvent(messageEvent);

expect(getByText(
moveModalMessages.moveModalTitle.defaultMessage.replace('{displayName}', requestData.title),
)).toBeInTheDocument();
expect(getByRole('button', { name: moveModalMessages.moveModalSubmitButton.defaultMessage })).toBeInTheDocument();
expect(getByRole('button', { name: moveModalMessages.moveModalCancelButton.defaultMessage })).toBeInTheDocument();
});

it('should navigates to xBlock current unit', async () => {
const {
getByText,
getByRole,
} = render(<RootWrapper />);

await waitFor(() => {
expect(getByText(unitDisplayName)).toBeInTheDocument();
});

axiosMock
.onGet(getOutlineInfo(courseId))
.reply(200, courseOutlineInfoMock);
await executeThunk(getCourseOutlineInfoQuery(courseId), store.dispatch);

window.dispatchEvent(messageEvent);

expect(getByText(
moveModalMessages.moveModalTitle.defaultMessage.replace('{displayName}', requestData.title),
)).toBeInTheDocument();

const currentSection = courseOutlineInfoMock.child_info.children[1];
const currentSectionItemText = getByRole('button', {
name: `${currentSection.display_name} ${moveModalMessages.moveModalOutlineItemCurrentLocationText.defaultMessage} ${moveModalMessages.moveModalOutlineItemViewText.defaultMessage}`,
});
expect(currentSectionItemText).toBeInTheDocument();
fireEvent.click(currentSectionItemText);

const currentSubsection = currentSection.child_info.children[0];
const currentSubsectionText = getByRole('button', {
name: `${currentSubsection.display_name} ${moveModalMessages.moveModalOutlineItemCurrentLocationText.defaultMessage} ${moveModalMessages.moveModalOutlineItemViewText.defaultMessage}`,
});
expect(currentSubsectionText).toBeInTheDocument();
fireEvent.click(currentSubsectionText);

const currentComponentLocationText = getByText(
moveModalMessages.moveModalOutlineItemCurrentComponentLocationText.defaultMessage,
);
expect(currentComponentLocationText).toBeInTheDocument();
});

it('should display "Move Confirmation" alert after moving and undo operations', async () => {
const {
queryByRole,
getByText,
} = render(<RootWrapper />);

axiosMock
.onPatch(postXBlockBaseApiUrl())
.reply(200, {});

await executeThunk(patchUnitItemQuery({
sourceLocator: requestData.sourceLocator,
targetParentLocator: requestData.targetParentLocator,
title: requestData.title,
currentParentLocator: requestData.currentParentLocator,
isMoving: requestData.isMoving,
callbackFn: requestData.callbackFn,
}), store.dispatch);

const dismissButton = queryByRole('button', {
name: /dismiss/i, hidden: true,
});
const undoButton = queryByRole('button', {
name: messages.undoMoveButton.defaultMessage, hidden: true,
});
const newLocationButton = queryByRole('button', {
name: messages.newLocationButton.defaultMessage, hidden: true,
});

expect(getByText(messages.alertMoveSuccessTitle.defaultMessage)).toBeInTheDocument();
expect(getByText(`${requestData.title} has been moved`)).toBeInTheDocument();
expect(dismissButton).toBeInTheDocument();
expect(undoButton).toBeInTheDocument();
expect(newLocationButton).toBeInTheDocument();
expect(requestData.callbackFn).toHaveBeenCalled();

fireEvent.click(undoButton);

await waitFor(() => {
expect(getByText(messages.alertMoveCancelTitle.defaultMessage)).toBeInTheDocument();
});
expect(getByText(
messages.alertMoveCancelDescription.defaultMessage.replace('{title}', requestData.title),
)).toBeInTheDocument();
expect(dismissButton).toBeInTheDocument();
expect(undoButton).not.toBeInTheDocument();
expect(newLocationButton).not.toBeInTheDocument();
expect(requestData.callbackFn).toHaveBeenCalled();
});

it('should navigate to new location by button click', async () => {
const {
queryByRole,
} = render(<RootWrapper />);

axiosMock
.onPatch(postXBlockBaseApiUrl())
.reply(200, {});

await executeThunk(patchUnitItemQuery({
sourceLocator: requestData.sourceLocator,
targetParentLocator: requestData.targetParentLocator,
title: requestData.title,
currentParentLocator: requestData.currentParentLocator,
isMoving: requestData.isMoving,
callbackFn: requestData.callbackFn,
}), store.dispatch);

const newLocationButton = queryByRole('button', {
name: messages.newLocationButton.defaultMessage, hidden: true,
});
fireEvent.click(newLocationButton);
expect(mockedUsedNavigate).toHaveBeenCalledWith(
`/course/${courseId}/container/${blockId}/${requestData.currentParentLocator}`,
{ replace: true },
);
});
});
});
Loading

0 comments on commit de9f97c

Please sign in to comment.