feat: confirmation dialog for closing any editor. TNL-10280.
This commit is contained in:
@@ -1,7 +1,40 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`EditorContainer component render snapshot: initialized. enable save and pass to header 1`] = `
|
||||
<div>
|
||||
<div
|
||||
className="position-relative zindex-0"
|
||||
>
|
||||
<BaseModal
|
||||
close={[MockFunction closeCancelConfirmModal]}
|
||||
confirmAction={
|
||||
<Button
|
||||
onClick={
|
||||
Object {
|
||||
"handleCancel": Object {
|
||||
"onClose": [MockFunction props.onClose],
|
||||
},
|
||||
}
|
||||
}
|
||||
variant="primary"
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="OK"
|
||||
description="Label for OK button"
|
||||
id="authoring.editorContainer.okButton.label"
|
||||
/>
|
||||
</Button>
|
||||
}
|
||||
footerAction={null}
|
||||
isOpen={false}
|
||||
size="md"
|
||||
title="Exit the editor?"
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="Are you sure you want to exit the editor? Any unsaved changes will be lost."
|
||||
description="Description text for modal confirming cancellation"
|
||||
id="authoring.editorContainer.cancelConfirm.description"
|
||||
/>
|
||||
</BaseModal>
|
||||
<ModalDialog.Header
|
||||
className="shadow-sm zindex-10"
|
||||
>
|
||||
@@ -23,13 +56,7 @@ exports[`EditorContainer component render snapshot: initialized. enable save and
|
||||
>
|
||||
<IconButton
|
||||
iconAs="Icon"
|
||||
onClick={
|
||||
Object {
|
||||
"handleCancelClicked": Object {
|
||||
"onClose": [MockFunction props.onClose],
|
||||
},
|
||||
}
|
||||
}
|
||||
onClick={[MockFunction openCancelConfirmModal]}
|
||||
src={[MockFunction icons.Close]}
|
||||
/>
|
||||
</div>
|
||||
@@ -40,13 +67,7 @@ exports[`EditorContainer component render snapshot: initialized. enable save and
|
||||
</h1>
|
||||
<injectIntl(ShimmedIntlComponent)
|
||||
disableSave={false}
|
||||
onCancel={
|
||||
Object {
|
||||
"handleCancelClicked": Object {
|
||||
"onClose": [MockFunction props.onClose],
|
||||
},
|
||||
}
|
||||
}
|
||||
onCancel={[MockFunction openCancelConfirmModal]}
|
||||
onSave={
|
||||
Object {
|
||||
"handleSaveClicked": Object {
|
||||
@@ -61,7 +82,40 @@ exports[`EditorContainer component render snapshot: initialized. enable save and
|
||||
`;
|
||||
|
||||
exports[`EditorContainer component render snapshot: not initialized. disable save and pass to header 1`] = `
|
||||
<div>
|
||||
<div
|
||||
className="position-relative zindex-0"
|
||||
>
|
||||
<BaseModal
|
||||
close={[MockFunction closeCancelConfirmModal]}
|
||||
confirmAction={
|
||||
<Button
|
||||
onClick={
|
||||
Object {
|
||||
"handleCancel": Object {
|
||||
"onClose": [MockFunction props.onClose],
|
||||
},
|
||||
}
|
||||
}
|
||||
variant="primary"
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="OK"
|
||||
description="Label for OK button"
|
||||
id="authoring.editorContainer.okButton.label"
|
||||
/>
|
||||
</Button>
|
||||
}
|
||||
footerAction={null}
|
||||
isOpen={false}
|
||||
size="md"
|
||||
title="Exit the editor?"
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="Are you sure you want to exit the editor? Any unsaved changes will be lost."
|
||||
description="Description text for modal confirming cancellation"
|
||||
id="authoring.editorContainer.cancelConfirm.description"
|
||||
/>
|
||||
</BaseModal>
|
||||
<ModalDialog.Header
|
||||
className="shadow-sm zindex-10"
|
||||
>
|
||||
@@ -83,13 +137,7 @@ exports[`EditorContainer component render snapshot: not initialized. disable sav
|
||||
>
|
||||
<IconButton
|
||||
iconAs="Icon"
|
||||
onClick={
|
||||
Object {
|
||||
"handleCancelClicked": Object {
|
||||
"onClose": [MockFunction props.onClose],
|
||||
},
|
||||
}
|
||||
}
|
||||
onClick={[MockFunction openCancelConfirmModal]}
|
||||
src={[MockFunction icons.Close]}
|
||||
/>
|
||||
</div>
|
||||
@@ -97,13 +145,7 @@ exports[`EditorContainer component render snapshot: not initialized. disable sav
|
||||
</ModalDialog.Header>
|
||||
<injectIntl(ShimmedIntlComponent)
|
||||
disableSave={true}
|
||||
onCancel={
|
||||
Object {
|
||||
"handleCancelClicked": Object {
|
||||
"onClose": [MockFunction props.onClose],
|
||||
},
|
||||
}
|
||||
}
|
||||
onCancel={[MockFunction openCancelConfirmModal]}
|
||||
onSave={
|
||||
Object {
|
||||
"handleSaveClicked": Object {
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
import { useSelector } from 'react-redux';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { RequestKeys } from '../../data/constants/requests';
|
||||
import { selectors } from '../../data/redux';
|
||||
import * as appHooks from '../../hooks';
|
||||
import * as module from './hooks';
|
||||
import analyticsEvt from '../../data/constants/analyticsEvt';
|
||||
import { StrictDict } from '../../utils';
|
||||
|
||||
export const {
|
||||
navigateCallback,
|
||||
@@ -11,6 +14,10 @@ export const {
|
||||
saveBlock,
|
||||
} = appHooks;
|
||||
|
||||
export const state = StrictDict({
|
||||
isCancelConfirmModalOpen: (val) => useState(val),
|
||||
});
|
||||
|
||||
export const handleSaveClicked = ({ dispatch, getContent, validateEntry }) => {
|
||||
const destination = useSelector(selectors.app.returnUrl);
|
||||
const analytics = useSelector(selectors.app.analytics);
|
||||
@@ -23,7 +30,16 @@ export const handleSaveClicked = ({ dispatch, getContent, validateEntry }) => {
|
||||
validateEntry,
|
||||
});
|
||||
};
|
||||
export const handleCancelClicked = ({ onClose }) => {
|
||||
export const cancelConfirmModalToggle = () => {
|
||||
const [isCancelConfirmOpen, setIsOpen] = module.state.isCancelConfirmModalOpen(false);
|
||||
return {
|
||||
isCancelConfirmOpen,
|
||||
openCancelConfirmModal: () => setIsOpen(true),
|
||||
closeCancelConfirmModal: () => setIsOpen(false),
|
||||
};
|
||||
};
|
||||
|
||||
export const handleCancel = ({ onClose }) => {
|
||||
if (onClose) {
|
||||
return onClose;
|
||||
}
|
||||
@@ -34,6 +50,6 @@ export const handleCancelClicked = ({ onClose }) => {
|
||||
});
|
||||
};
|
||||
export const isInitialized = () => useSelector(selectors.app.isInitialized);
|
||||
export const saveFailed = () => useSelector((state) => (
|
||||
selectors.requests.isFailed(state, { requestKey: RequestKeys.saveBlock })
|
||||
export const saveFailed = () => useSelector((rootState) => (
|
||||
selectors.requests.isFailed(rootState, { requestKey: RequestKeys.saveBlock })
|
||||
));
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import * as reactRedux from 'react-redux';
|
||||
import { MockUseState } from '../../../testUtils';
|
||||
|
||||
import { RequestKeys } from '../../data/constants/requests';
|
||||
import { selectors } from '../../data/redux';
|
||||
@@ -7,6 +8,8 @@ import * as appHooks from '../../hooks';
|
||||
import * as hooks from './hooks';
|
||||
import analyticsEvt from '../../data/constants/analyticsEvt';
|
||||
|
||||
const hookState = new MockUseState(hooks);
|
||||
|
||||
jest.mock('../../data/redux', () => ({
|
||||
selectors: {
|
||||
app: {
|
||||
@@ -67,9 +70,46 @@ describe('EditorContainer hooks', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
describe('handleCancelClicked', () => {
|
||||
|
||||
describe('cancelConfirmModalToggle', () => {
|
||||
const hookKey = hookState.keys.isCancelConfirmModalOpen;
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
describe('state hook', () => {
|
||||
hookState.testGetter(hookKey);
|
||||
});
|
||||
describe('using state', () => {
|
||||
beforeEach(() => {
|
||||
hookState.mock();
|
||||
});
|
||||
afterEach(() => {
|
||||
hookState.restore();
|
||||
});
|
||||
|
||||
describe('cancelConfirmModalToggle', () => {
|
||||
let hook;
|
||||
beforeEach(() => {
|
||||
hook = hooks.cancelConfirmModalToggle();
|
||||
});
|
||||
test('isCancelConfirmOpen: state value', () => {
|
||||
expect(hook.isCancelConfirmOpen).toEqual(hookState.stateVals[hookKey]);
|
||||
});
|
||||
test('openCancelConfirmModal: calls setter with true', () => {
|
||||
hook.openCancelConfirmModal();
|
||||
expect(hookState.setState[hookKey]).toHaveBeenCalledWith(true);
|
||||
});
|
||||
test('closeCancelConfirmModal: calls setter with false', () => {
|
||||
hook.closeCancelConfirmModal();
|
||||
expect(hookState.setState[hookKey]).toHaveBeenCalledWith(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('handleCancel', () => {
|
||||
it('calls navigateCallback to returnUrl if onClose is not passed', () => {
|
||||
expect(hooks.handleCancelClicked({})).toEqual(
|
||||
expect(hooks.handleCancel({})).toEqual(
|
||||
appHooks.navigateCallback({
|
||||
destination: reactRedux.useSelector(selectors.app.returnUrl),
|
||||
analyticsEvent: analyticsEvt.editorCancelClick,
|
||||
@@ -79,7 +119,7 @@ describe('EditorContainer hooks', () => {
|
||||
});
|
||||
it('calls onClose and not navigateCallback if onClose is passed', () => {
|
||||
const onClose = () => 'my close value';
|
||||
expect(hooks.handleCancelClicked({ onClose })).toEqual(onClose);
|
||||
expect(hooks.handleCancel({ onClose })).toEqual(onClose);
|
||||
expect(appHooks.navigateCallback).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -2,24 +2,50 @@ import React from 'react';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { Icon, ModalDialog, IconButton } from '@edx/paragon';
|
||||
import {
|
||||
Icon, ModalDialog, IconButton, Button,
|
||||
} from '@edx/paragon';
|
||||
import { Close } from '@edx/paragon/icons';
|
||||
|
||||
import { injectIntl, intlShape, FormattedMessage } from '@edx/frontend-platform/i18n';
|
||||
import EditorFooter from './components/EditorFooter';
|
||||
import TitleHeader from './components/TitleHeader';
|
||||
import * as hooks from './hooks';
|
||||
import BaseModal from '../TextEditor/components/BaseModal';
|
||||
import messages from './messages';
|
||||
|
||||
export const EditorContainer = ({
|
||||
children,
|
||||
getContent,
|
||||
onClose,
|
||||
validateEntry,
|
||||
// injected
|
||||
intl,
|
||||
}) => {
|
||||
const dispatch = useDispatch();
|
||||
const isInitialized = hooks.isInitialized();
|
||||
const handleCancelClicked = hooks.handleCancelClicked({ onClose });
|
||||
const { isCancelConfirmOpen, openCancelConfirmModal, closeCancelConfirmModal } = hooks.cancelConfirmModalToggle();
|
||||
const handleCancel = hooks.handleCancel({ onClose });
|
||||
return (
|
||||
<div>
|
||||
<div
|
||||
className="position-relative zindex-0"
|
||||
>
|
||||
<BaseModal
|
||||
size="md"
|
||||
confirmAction={(
|
||||
<Button
|
||||
variant="primary"
|
||||
onClick={handleCancel}
|
||||
>
|
||||
<FormattedMessage {...messages.okButtonLabel} />
|
||||
</Button>
|
||||
)}
|
||||
isOpen={isCancelConfirmOpen}
|
||||
close={closeCancelConfirmModal}
|
||||
title={intl.formatMessage(messages.cancelConfirmTitle)}
|
||||
>
|
||||
<FormattedMessage {...messages.cancelConfirmDescription} />
|
||||
</BaseModal>
|
||||
<ModalDialog.Header className="shadow-sm zindex-10">
|
||||
<ModalDialog.Title>
|
||||
<div
|
||||
@@ -31,14 +57,14 @@ export const EditorContainer = ({
|
||||
<IconButton
|
||||
src={Close}
|
||||
iconAs={Icon}
|
||||
onClick={handleCancelClicked}
|
||||
onClick={openCancelConfirmModal}
|
||||
/>
|
||||
</div>
|
||||
</ModalDialog.Title>
|
||||
</ModalDialog.Header>
|
||||
{isInitialized && children}
|
||||
<EditorFooter
|
||||
onCancel={handleCancelClicked}
|
||||
onCancel={openCancelConfirmModal}
|
||||
onSave={hooks.handleSaveClicked({ dispatch, getContent, validateEntry })}
|
||||
disableSave={!isInitialized}
|
||||
saveFailed={hooks.saveFailed()}
|
||||
@@ -55,6 +81,8 @@ EditorContainer.propTypes = {
|
||||
getContent: PropTypes.func.isRequired,
|
||||
onClose: PropTypes.func,
|
||||
validateEntry: PropTypes.func,
|
||||
// injected
|
||||
intl: intlShape.isRequired,
|
||||
};
|
||||
|
||||
export default EditorContainer;
|
||||
export default injectIntl(EditorContainer);
|
||||
|
||||
@@ -1,21 +1,28 @@
|
||||
import { IconButton } from '@edx/paragon';
|
||||
import { shallow } from 'enzyme';
|
||||
import { useDispatch } from 'react-redux';
|
||||
|
||||
import { EditorContainer } from '.';
|
||||
import * as hooks from './hooks';
|
||||
import { formatMessage } from '../../../testUtils';
|
||||
|
||||
const props = {
|
||||
getContent: jest.fn().mockName('props.getContent'),
|
||||
onClose: jest.fn().mockName('props.onClose'),
|
||||
validateEntry: jest.fn().mockName('props.validateEntry'),
|
||||
// inject
|
||||
intl: { formatMessage },
|
||||
};
|
||||
|
||||
jest.mock('./hooks', () => ({
|
||||
isInitialized: jest.fn().mockReturnValue(true),
|
||||
handleCancelClicked: (args) => ({ handleCancelClicked: args }),
|
||||
handleCancel: (args) => ({ handleCancel: args }),
|
||||
handleSaveClicked: (args) => ({ handleSaveClicked: args }),
|
||||
saveFailed: jest.fn().mockName('hooks.saveFailed'),
|
||||
cancelConfirmModalToggle: jest.fn(() => ({
|
||||
isCancelConfirmOpen: false,
|
||||
openCancelConfirmModal: jest.fn().mockName('openCancelConfirmModal'),
|
||||
closeCancelConfirmModal: jest.fn().mockName('closeCancelConfirmModal'),
|
||||
})),
|
||||
}));
|
||||
|
||||
let el;
|
||||
@@ -39,23 +46,13 @@ describe('EditorContainer component', () => {
|
||||
el = shallow(<EditorContainer {...props}>{testContent}</EditorContainer>);
|
||||
});
|
||||
|
||||
test('close behavior is linked to modal onClose', () => {
|
||||
const expected = hooks.handleCancelClicked({ onClose: props.onClose });
|
||||
expect(el.find(IconButton)
|
||||
.props().onClick).toEqual(expected);
|
||||
});
|
||||
test('close behavior is linked to footer onCancel', () => {
|
||||
const expected = hooks.handleCancelClicked({ onClose: props.onClose });
|
||||
expect(el.children().at(2)
|
||||
.props().onCancel).toEqual(expected);
|
||||
});
|
||||
test('save behavior is linked to footer onSave', () => {
|
||||
const expected = hooks.handleSaveClicked({
|
||||
dispatch: useDispatch(),
|
||||
getContent: props.getContent,
|
||||
validateEntry: props.validateEntry,
|
||||
});
|
||||
expect(el.children().at(2)
|
||||
expect(el.children().at(3)
|
||||
.props().onSave).toEqual(expected);
|
||||
});
|
||||
});
|
||||
|
||||
19
src/editors/containers/EditorContainer/messages.js
Normal file
19
src/editors/containers/EditorContainer/messages.js
Normal file
@@ -0,0 +1,19 @@
|
||||
export const messages = {
|
||||
cancelConfirmTitle: {
|
||||
id: 'authoring.editorContainer.cancelConfirm.title',
|
||||
defaultMessage: 'Exit the editor?',
|
||||
description: 'Label for modal confirming cancellation',
|
||||
},
|
||||
cancelConfirmDescription: {
|
||||
id: 'authoring.editorContainer.cancelConfirm.description',
|
||||
defaultMessage: 'Are you sure you want to exit the editor? Any unsaved changes will be lost.',
|
||||
description: 'Description text for modal confirming cancellation',
|
||||
},
|
||||
okButtonLabel: {
|
||||
id: 'authoring.editorContainer.okButton.label',
|
||||
defaultMessage: 'OK',
|
||||
description: 'Label for OK button',
|
||||
},
|
||||
};
|
||||
|
||||
export default messages;
|
||||
@@ -12,7 +12,7 @@ export const SelectTypeWrapper = ({
|
||||
onClose,
|
||||
selected,
|
||||
}) => {
|
||||
const handleCancelClicked = hooks.handleCancelClicked({ onClose });
|
||||
const handleCancel = hooks.handleCancel({ onClose });
|
||||
|
||||
return (
|
||||
<div>
|
||||
@@ -23,7 +23,7 @@ export const SelectTypeWrapper = ({
|
||||
<IconButton
|
||||
src={Close}
|
||||
iconAs={Icon}
|
||||
onClick={handleCancelClicked}
|
||||
onClick={handleCancel}
|
||||
/>
|
||||
</div>
|
||||
</ModalDialog.Title>
|
||||
@@ -33,7 +33,7 @@ export const SelectTypeWrapper = ({
|
||||
</ModalDialog.Body>
|
||||
<SelectTypeFooter
|
||||
selected={selected}
|
||||
onCancel={handleCancelClicked}
|
||||
onCancel={handleCancel}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -2,10 +2,10 @@ import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
import { IconButton } from '@edx/paragon';
|
||||
import * as module from '.';
|
||||
import { handleCancelClicked } from '../../../../EditorContainer/hooks';
|
||||
import { handleCancel } from '../../../../EditorContainer/hooks';
|
||||
|
||||
jest.mock('../../../../EditorContainer/hooks', () => ({
|
||||
handleCancelClicked: jest.fn().mockName('handleCancelClicked'),
|
||||
handleCancel: jest.fn().mockName('handleCancel'),
|
||||
}));
|
||||
|
||||
describe('SelectTypeWrapper', () => {
|
||||
@@ -25,7 +25,7 @@ describe('SelectTypeWrapper', () => {
|
||||
el = shallow(<module.SelectTypeWrapper {...props} />);
|
||||
});
|
||||
test('close behavior is linked to modal onClose', () => {
|
||||
const expected = handleCancelClicked({ onClose: props.onClose });
|
||||
const expected = handleCancel({ onClose: props.onClose });
|
||||
expect(el.find(IconButton).props().onClick)
|
||||
.toEqual(expected);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user