feat: Add cancel confirmation modal to Advanced editors in libraries [FC-0076] (#1672)

* Extracts the cancel confirmation modal as a new component.
* Adds the cancel confirmation modal to the Advanced editors in libraries.
This commit is contained in:
Chris Chávez
2025-02-21 11:24:23 -05:00
committed by GitHub
parent 7e4ecff4e8
commit 0db1727537
4 changed files with 81 additions and 33 deletions

View File

@@ -4,6 +4,9 @@ import {
render,
initializeMocks,
waitFor,
screen,
act,
fireEvent,
} from '../testUtils';
import AdvancedEditor from './AdvancedEditor';
@@ -17,7 +20,7 @@ describe('AdvancedEditor', () => {
initializeMocks();
});
it('should call onClose when receiving "cancel-clicked" message', () => {
it('should call onClose when receiving "cancel-clicked" message', async () => {
render(<AdvancedEditor usageKey="test" onClose={onCloseMock} />);
const messageEvent = new MessageEvent('message', {
@@ -28,8 +31,17 @@ describe('AdvancedEditor', () => {
origin: getConfig().STUDIO_BASE_URL,
});
window.dispatchEvent(messageEvent);
act(() => {
// Send cancel event
window.dispatchEvent(messageEvent);
});
// Expect open cancel confimation modal
expect(await screen.findByText(/Are you sure you want to exit the editor/)).toBeInTheDocument();
// Click on "OK"
const confirmButton = await screen.findByRole('button', { name: 'OK' });
fireEvent.click(confirmButton);
// Should call `onClose`
expect(onCloseMock).toHaveBeenCalled();
});

View File

@@ -1,20 +1,24 @@
import React, { useEffect } from 'react';
import { getConfig } from '@edx/frontend-platform';
import { useIntl } from '@edx/frontend-platform/i18n';
import { useToggle } from '@openedx/paragon';
import { LibraryBlock } from '../library-authoring/LibraryBlock';
import { EditorModalWrapper } from './containers/EditorContainer';
import { ToastContext } from '../generic/toast-context';
import messages from './messages';
import CancelConfirmModal from './containers/EditorContainer/components/CancelConfirmModal';
interface AdvancedEditorProps {
usageKey: string,
onClose: Function | null,
onClose: (() => void) | null,
}
const AdvancedEditor = ({ usageKey, onClose }: AdvancedEditorProps) => {
const intl = useIntl();
const { showToast } = React.useContext(ToastContext);
const [isCancelConfirmOpen, openCancelConfirmModal, closeCancelConfirmModal] = useToggle(false);
useEffect(() => {
const handleIframeMessage = (event) => {
@@ -25,9 +29,9 @@ const AdvancedEditor = ({ usageKey, onClose }: AdvancedEditorProps) => {
if (event.data.type === 'xblock-event') {
const { eventName, data } = event.data;
if (onClose && (eventName === 'cancel'
|| (eventName === 'save' && data.state === 'end'))
) {
if (eventName === 'cancel') {
openCancelConfirmModal();
} else if (onClose && eventName === 'save' && data.state === 'end') {
onClose();
} else if (eventName === 'error') {
showToast(intl.formatMessage(messages.advancedEditorGenericError));
@@ -43,12 +47,19 @@ const AdvancedEditor = ({ usageKey, onClose }: AdvancedEditorProps) => {
}, []);
return (
<EditorModalWrapper onClose={onClose as () => void}>
<LibraryBlock
usageKey={usageKey}
view="studio_view"
<>
<EditorModalWrapper onClose={openCancelConfirmModal}>
<LibraryBlock
usageKey={usageKey}
view="studio_view"
/>
</EditorModalWrapper>
<CancelConfirmModal
isOpen={isCancelConfirmOpen}
closeCancelConfirmModal={closeCancelConfirmModal}
onCloseEditor={onClose}
/>
</EditorModalWrapper>
</>
);
};

View File

@@ -0,0 +1,38 @@
import { Button } from '@openedx/paragon';
import { useIntl, FormattedMessage } from '@edx/frontend-platform/i18n';
import BaseModal from '../../../sharedComponents/BaseModal';
import messages from '../messages';
interface CancelConfirmModalProps {
isOpen: boolean,
closeCancelConfirmModal: () => void,
onCloseEditor: (() => void) | null,
}
const CancelConfirmModal = ({
isOpen,
closeCancelConfirmModal,
onCloseEditor,
}: CancelConfirmModalProps) => {
const intl = useIntl();
return (
<BaseModal
size="md"
confirmAction={(
<Button
variant="primary"
onClick={() => onCloseEditor?.()}
>
<FormattedMessage {...messages.okButtonLabel} />
</Button>
)}
isOpen={isOpen}
close={closeCancelConfirmModal}
title={intl.formatMessage(messages.cancelConfirmTitle)}
>
<FormattedMessage {...messages.cancelConfirmDescription} />
</BaseModal>
);
};
export default CancelConfirmModal;

View File

@@ -15,12 +15,12 @@ import { useIntl, FormattedMessage } from '@edx/frontend-platform/i18n';
import { EditorComponent } from '../../EditorComponent';
import { useEditorContext } from '../../EditorContext';
import BaseModal from '../../sharedComponents/BaseModal';
import TitleHeader from './components/TitleHeader';
import * as hooks from './hooks';
import messages from './messages';
import './index.scss';
import usePromptIfDirty from '../../../generic/promptIfDirty/usePromptIfDirty';
import CancelConfirmModal from './components/CancelConfirmModal';
interface WrapperProps {
children: React.ReactNode;
@@ -118,29 +118,16 @@ const EditorContainer: React.FC<Props> = ({
<FormattedMessage {...messages.contentSaveFailed} />
</Toast>
)}
<BaseModal
size="md"
confirmAction={(
<Button
variant="primary"
onClick={() => {
handleCancel();
if (returnFunction) {
closeCancelConfirmModal();
}
}}
>
<FormattedMessage {...messages.okButtonLabel} />
</Button>
)}
<CancelConfirmModal
isOpen={isCancelConfirmOpen}
close={() => {
closeCancelConfirmModal();
closeCancelConfirmModal={closeCancelConfirmModal}
onCloseEditor={() => {
handleCancel();
if (returnFunction) {
closeCancelConfirmModal();
}
}}
title={intl.formatMessage(messages.cancelConfirmTitle)}
>
<FormattedMessage {...messages.cancelConfirmDescription} />
</BaseModal>
/>
<ModalDialog.Header className="shadow-sm zindex-10">
<div className="d-flex flex-row justify-content-between">
<h2 className="h3 col pl-0">