Skip to content

Commit 0db1727

Browse files
authored
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.
1 parent 7e4ecff commit 0db1727

4 files changed

Lines changed: 81 additions & 33 deletions

File tree

src/editors/AdvancedEditor.test.tsx

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@ import {
44
render,
55
initializeMocks,
66
waitFor,
7+
screen,
8+
act,
9+
fireEvent,
710
} from '../testUtils';
811
import AdvancedEditor from './AdvancedEditor';
912

@@ -17,7 +20,7 @@ describe('AdvancedEditor', () => {
1720
initializeMocks();
1821
});
1922

20-
it('should call onClose when receiving "cancel-clicked" message', () => {
23+
it('should call onClose when receiving "cancel-clicked" message', async () => {
2124
render(<AdvancedEditor usageKey="test" onClose={onCloseMock} />);
2225

2326
const messageEvent = new MessageEvent('message', {
@@ -28,8 +31,17 @@ describe('AdvancedEditor', () => {
2831
origin: getConfig().STUDIO_BASE_URL,
2932
});
3033

31-
window.dispatchEvent(messageEvent);
34+
act(() => {
35+
// Send cancel event
36+
window.dispatchEvent(messageEvent);
37+
});
3238

39+
// Expect open cancel confimation modal
40+
expect(await screen.findByText(/Are you sure you want to exit the editor/)).toBeInTheDocument();
41+
// Click on "OK"
42+
const confirmButton = await screen.findByRole('button', { name: 'OK' });
43+
fireEvent.click(confirmButton);
44+
// Should call `onClose`
3345
expect(onCloseMock).toHaveBeenCalled();
3446
});
3547

src/editors/AdvancedEditor.tsx

Lines changed: 20 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,24 @@
11
import React, { useEffect } from 'react';
22
import { getConfig } from '@edx/frontend-platform';
33
import { useIntl } from '@edx/frontend-platform/i18n';
4+
import { useToggle } from '@openedx/paragon';
45

56
import { LibraryBlock } from '../library-authoring/LibraryBlock';
67
import { EditorModalWrapper } from './containers/EditorContainer';
78
import { ToastContext } from '../generic/toast-context';
9+
810
import messages from './messages';
11+
import CancelConfirmModal from './containers/EditorContainer/components/CancelConfirmModal';
912

1013
interface AdvancedEditorProps {
1114
usageKey: string,
12-
onClose: Function | null,
15+
onClose: (() => void) | null,
1316
}
1417

1518
const AdvancedEditor = ({ usageKey, onClose }: AdvancedEditorProps) => {
1619
const intl = useIntl();
1720
const { showToast } = React.useContext(ToastContext);
21+
const [isCancelConfirmOpen, openCancelConfirmModal, closeCancelConfirmModal] = useToggle(false);
1822

1923
useEffect(() => {
2024
const handleIframeMessage = (event) => {
@@ -25,9 +29,9 @@ const AdvancedEditor = ({ usageKey, onClose }: AdvancedEditorProps) => {
2529
if (event.data.type === 'xblock-event') {
2630
const { eventName, data } = event.data;
2731

28-
if (onClose && (eventName === 'cancel'
29-
|| (eventName === 'save' && data.state === 'end'))
30-
) {
32+
if (eventName === 'cancel') {
33+
openCancelConfirmModal();
34+
} else if (onClose && eventName === 'save' && data.state === 'end') {
3135
onClose();
3236
} else if (eventName === 'error') {
3337
showToast(intl.formatMessage(messages.advancedEditorGenericError));
@@ -43,12 +47,19 @@ const AdvancedEditor = ({ usageKey, onClose }: AdvancedEditorProps) => {
4347
}, []);
4448

4549
return (
46-
<EditorModalWrapper onClose={onClose as () => void}>
47-
<LibraryBlock
48-
usageKey={usageKey}
49-
view="studio_view"
50+
<>
51+
<EditorModalWrapper onClose={openCancelConfirmModal}>
52+
<LibraryBlock
53+
usageKey={usageKey}
54+
view="studio_view"
55+
/>
56+
</EditorModalWrapper>
57+
<CancelConfirmModal
58+
isOpen={isCancelConfirmOpen}
59+
closeCancelConfirmModal={closeCancelConfirmModal}
60+
onCloseEditor={onClose}
5061
/>
51-
</EditorModalWrapper>
62+
</>
5263
);
5364
};
5465

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import { Button } from '@openedx/paragon';
2+
import { useIntl, FormattedMessage } from '@edx/frontend-platform/i18n';
3+
import BaseModal from '../../../sharedComponents/BaseModal';
4+
import messages from '../messages';
5+
6+
interface CancelConfirmModalProps {
7+
isOpen: boolean,
8+
closeCancelConfirmModal: () => void,
9+
onCloseEditor: (() => void) | null,
10+
}
11+
12+
const CancelConfirmModal = ({
13+
isOpen,
14+
closeCancelConfirmModal,
15+
onCloseEditor,
16+
}: CancelConfirmModalProps) => {
17+
const intl = useIntl();
18+
return (
19+
<BaseModal
20+
size="md"
21+
confirmAction={(
22+
<Button
23+
variant="primary"
24+
onClick={() => onCloseEditor?.()}
25+
>
26+
<FormattedMessage {...messages.okButtonLabel} />
27+
</Button>
28+
)}
29+
isOpen={isOpen}
30+
close={closeCancelConfirmModal}
31+
title={intl.formatMessage(messages.cancelConfirmTitle)}
32+
>
33+
<FormattedMessage {...messages.cancelConfirmDescription} />
34+
</BaseModal>
35+
);
36+
};
37+
38+
export default CancelConfirmModal;

src/editors/containers/EditorContainer/index.tsx

Lines changed: 9 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,12 @@ import { useIntl, FormattedMessage } from '@edx/frontend-platform/i18n';
1515

1616
import { EditorComponent } from '../../EditorComponent';
1717
import { useEditorContext } from '../../EditorContext';
18-
import BaseModal from '../../sharedComponents/BaseModal';
1918
import TitleHeader from './components/TitleHeader';
2019
import * as hooks from './hooks';
2120
import messages from './messages';
2221
import './index.scss';
2322
import usePromptIfDirty from '../../../generic/promptIfDirty/usePromptIfDirty';
23+
import CancelConfirmModal from './components/CancelConfirmModal';
2424

2525
interface WrapperProps {
2626
children: React.ReactNode;
@@ -118,29 +118,16 @@ const EditorContainer: React.FC<Props> = ({
118118
<FormattedMessage {...messages.contentSaveFailed} />
119119
</Toast>
120120
)}
121-
<BaseModal
122-
size="md"
123-
confirmAction={(
124-
<Button
125-
variant="primary"
126-
onClick={() => {
127-
handleCancel();
128-
if (returnFunction) {
129-
closeCancelConfirmModal();
130-
}
131-
}}
132-
>
133-
<FormattedMessage {...messages.okButtonLabel} />
134-
</Button>
135-
)}
121+
<CancelConfirmModal
136122
isOpen={isCancelConfirmOpen}
137-
close={() => {
138-
closeCancelConfirmModal();
123+
closeCancelConfirmModal={closeCancelConfirmModal}
124+
onCloseEditor={() => {
125+
handleCancel();
126+
if (returnFunction) {
127+
closeCancelConfirmModal();
128+
}
139129
}}
140-
title={intl.formatMessage(messages.cancelConfirmTitle)}
141-
>
142-
<FormattedMessage {...messages.cancelConfirmDescription} />
143-
</BaseModal>
130+
/>
144131
<ModalDialog.Header className="shadow-sm zindex-10">
145132
<div className="d-flex flex-row justify-content-between">
146133
<h2 className="h3 col pl-0">

0 commit comments

Comments
 (0)