From 0db1727537454504323d285b3cda55b71ecd04e4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Chris=20Ch=C3=A1vez?= Date: Fri, 21 Feb 2025 11:24:23 -0500 Subject: [PATCH] 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. --- src/editors/AdvancedEditor.test.tsx | 16 +++++++- src/editors/AdvancedEditor.tsx | 29 +++++++++----- .../components/CancelConfirmModal.tsx | 38 +++++++++++++++++++ .../containers/EditorContainer/index.tsx | 31 +++++---------- 4 files changed, 81 insertions(+), 33 deletions(-) create mode 100644 src/editors/containers/EditorContainer/components/CancelConfirmModal.tsx diff --git a/src/editors/AdvancedEditor.test.tsx b/src/editors/AdvancedEditor.test.tsx index fcbd4a886..635069f35 100644 --- a/src/editors/AdvancedEditor.test.tsx +++ b/src/editors/AdvancedEditor.test.tsx @@ -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(); 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(); }); diff --git a/src/editors/AdvancedEditor.tsx b/src/editors/AdvancedEditor.tsx index 17e6c82b0..5a14d2cce 100644 --- a/src/editors/AdvancedEditor.tsx +++ b/src/editors/AdvancedEditor.tsx @@ -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 ( - void}> - + + + + - + ); }; diff --git a/src/editors/containers/EditorContainer/components/CancelConfirmModal.tsx b/src/editors/containers/EditorContainer/components/CancelConfirmModal.tsx new file mode 100644 index 000000000..a0860c0fc --- /dev/null +++ b/src/editors/containers/EditorContainer/components/CancelConfirmModal.tsx @@ -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 ( + onCloseEditor?.()} + > + + + )} + isOpen={isOpen} + close={closeCancelConfirmModal} + title={intl.formatMessage(messages.cancelConfirmTitle)} + > + + + ); +}; + +export default CancelConfirmModal; diff --git a/src/editors/containers/EditorContainer/index.tsx b/src/editors/containers/EditorContainer/index.tsx index eb9e08ab2..bc22c360d 100644 --- a/src/editors/containers/EditorContainer/index.tsx +++ b/src/editors/containers/EditorContainer/index.tsx @@ -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 = ({ )} - { - handleCancel(); - if (returnFunction) { - closeCancelConfirmModal(); - } - }} - > - - - )} + { - closeCancelConfirmModal(); + closeCancelConfirmModal={closeCancelConfirmModal} + onCloseEditor={() => { + handleCancel(); + if (returnFunction) { + closeCancelConfirmModal(); + } }} - title={intl.formatMessage(messages.cancelConfirmTitle)} - > - - + />