diff --git a/src/editors/containers/EditorContainer/__snapshots__/index.test.jsx.snap b/src/editors/containers/EditorContainer/__snapshots__/index.test.jsx.snap index 19f735d73..dc5051f11 100644 --- a/src/editors/containers/EditorContainer/__snapshots__/index.test.jsx.snap +++ b/src/editors/containers/EditorContainer/__snapshots__/index.test.jsx.snap @@ -41,6 +41,7 @@ exports[`EditorContainer component render snapshot: initialized. enable save and "handleSaveClicked": Object { "dispatch": [MockFunction react-redux.dispatch], "getContent": [MockFunction props.getContent], + "validateEntry": [MockFunction props.validateEntry], }, } } @@ -86,6 +87,7 @@ exports[`EditorContainer component render snapshot: not initialized. disable sav "handleSaveClicked": Object { "dispatch": [MockFunction react-redux.dispatch], "getContent": [MockFunction props.getContent], + "validateEntry": [MockFunction props.validateEntry], }, } } diff --git a/src/editors/containers/EditorContainer/hooks.js b/src/editors/containers/EditorContainer/hooks.js index 32f3a8346..77eae480a 100644 --- a/src/editors/containers/EditorContainer/hooks.js +++ b/src/editors/containers/EditorContainer/hooks.js @@ -23,7 +23,7 @@ export const setAssetToStaticUrl = (images, getContent) => { imgsArray.forEach(image => { imageUrls.push({ portableUrl: image.portableUrl, displayName: image.displayName }); }); - const imageSrcs = content.split('src="'); + const imageSrcs = typeof content === 'string' ? content.split('src="') : []; imageSrcs.forEach(src => { if (src.startsWith('/asset') && imageUrls.length > 0) { const nameFromEditorSrc = src.substring(src.lastIndexOf('@') + 1, src.indexOf('"')); @@ -44,15 +44,16 @@ export const setAssetToStaticUrl = (images, getContent) => { return content; }; -export const handleSaveClicked = ({ getContent, dispatch }) => { +export const handleSaveClicked = ({ dispatch, getContent, validateEntry }) => { const destination = useSelector(selectors.app.returnUrl); const analytics = useSelector(selectors.app.analytics); const images = useSelector(selectors.app.images); return () => saveBlock({ + analytics, content: setAssetToStaticUrl(images, getContent), destination, - analytics, dispatch, + validateEntry, }); }; export const handleCancelClicked = ({ onClose }) => { diff --git a/src/editors/containers/EditorContainer/hooks.test.jsx b/src/editors/containers/EditorContainer/hooks.test.jsx index 3929d4bb2..a9f027017 100644 --- a/src/editors/containers/EditorContainer/hooks.test.jsx +++ b/src/editors/containers/EditorContainer/hooks.test.jsx @@ -59,12 +59,17 @@ describe('EditorContainer hooks', () => { it('returns callback to saveBlock with dispatch and content from setAssetToStaticUrl', () => { const getContent = () => 'myTestContentValue'; const setAssetToStaticUrl = () => 'myTestContentValue'; + const validateEntry = () => 'vaLIdAteENTry'; const output = hooks.handleSaveClicked({ getContent, - images: { portableUrl: '/static/sOmEuiMAge.jpeg', displayName: 'sOmEuiMAge' }, + images: { + portableUrl: '/static/sOmEuiMAge.jpeg', + displayName: 'sOmEuiMAge', + }, destination: 'testDEsTURL', analytics: 'soMEanALytics', dispatch, + validateEntry, }); output(); expect(appHooks.saveBlock).toHaveBeenCalledWith({ @@ -72,6 +77,7 @@ describe('EditorContainer hooks', () => { destination: reactRedux.useSelector(selectors.app.returnUrl), analytics: reactRedux.useSelector(selectors.app.analytics), dispatch, + validateEntry, }); }); }); diff --git a/src/editors/containers/EditorContainer/index.jsx b/src/editors/containers/EditorContainer/index.jsx index c554781f3..8d8258cc1 100644 --- a/src/editors/containers/EditorContainer/index.jsx +++ b/src/editors/containers/EditorContainer/index.jsx @@ -13,6 +13,7 @@ export const EditorContainer = ({ children, getContent, onClose, + validateEntry, }) => { const dispatch = useDispatch(); const isInitialized = hooks.isInitialized(); @@ -34,7 +35,7 @@ export const EditorContainer = ({ {isInitialized && children} @@ -43,11 +44,13 @@ export const EditorContainer = ({ }; EditorContainer.defaultProps = { onClose: null, + validateEntry: null, }; EditorContainer.propTypes = { - getContent: PropTypes.func.isRequired, children: PropTypes.node.isRequired, + getContent: PropTypes.func.isRequired, onClose: PropTypes.func, + validateEntry: PropTypes.func, }; export default EditorContainer; diff --git a/src/editors/containers/EditorContainer/index.test.jsx b/src/editors/containers/EditorContainer/index.test.jsx index 6b52550a6..d1bcd73cd 100644 --- a/src/editors/containers/EditorContainer/index.test.jsx +++ b/src/editors/containers/EditorContainer/index.test.jsx @@ -8,6 +8,7 @@ import * as hooks from './hooks'; const props = { getContent: jest.fn().mockName('props.getContent'), onClose: jest.fn().mockName('props.onClose'), + validateEntry: jest.fn().mockName('props.validateEntry'), }; jest.mock('./hooks', () => ({ @@ -50,8 +51,9 @@ describe('EditorContainer component', () => { }); test('save behavior is linked to footer onSave', () => { const expected = hooks.handleSaveClicked({ - getContent: props.getContent, dispatch: useDispatch(), + getContent: props.getContent, + validateEntry: props.validateEntry, }); expect(el.children().at(2) .props().onSave).toEqual(expected); diff --git a/src/editors/containers/VideoEditor/components/VideoEditorModal.jsx b/src/editors/containers/VideoEditor/components/VideoEditorModal.jsx index bd7a7a4bb..cff3201c3 100644 --- a/src/editors/containers/VideoEditor/components/VideoEditorModal.jsx +++ b/src/editors/containers/VideoEditor/components/VideoEditorModal.jsx @@ -16,21 +16,31 @@ export const hooks = { }; const VideoEditorModal = ({ - isOpen, close, + error, + isOpen, }) => { const dispatch = useDispatch(); module.hooks.initialize(dispatch); return ( - + ); // TODO: add logic to show SelectVideoModal if no selection }; VideoEditorModal.defaultProps = { + error: { + duration: {}, + handout: {}, + license: {}, + thumbnail: {}, + transcripts: {}, + videoSource: {}, + }, }; VideoEditorModal.propTypes = { - isOpen: PropTypes.bool.isRequired, close: PropTypes.func.isRequired, + error: PropTypes.node, + isOpen: PropTypes.bool.isRequired, }; export default VideoEditorModal; diff --git a/src/editors/containers/VideoEditor/components/VideoSettingsModal/components/CollapsibleFormWidget.jsx b/src/editors/containers/VideoEditor/components/VideoSettingsModal/components/CollapsibleFormWidget.jsx index 70f6bd1e2..bd578754a 100644 --- a/src/editors/containers/VideoEditor/components/VideoSettingsModal/components/CollapsibleFormWidget.jsx +++ b/src/editors/containers/VideoEditor/components/VideoSettingsModal/components/CollapsibleFormWidget.jsx @@ -1,7 +1,11 @@ import React from 'react'; import PropTypes from 'prop-types'; -import { Collapsible } from '@edx/paragon'; +import { injectIntl, intlShape } from '@edx/frontend-platform/i18n'; +import { Collapsible, Icon, IconButton } from '@edx/paragon'; +import { ExpandLess, ExpandMore, Info } from '@edx/paragon/icons'; + +import messages from './messages'; /** * Simple Wrapper for a Form Widget component in the Video Settings modal @@ -11,16 +15,56 @@ import { Collapsible } from '@edx/paragon'; * */ export const CollapsibleFormWidget = ({ - title, children, + isError, + subtitle, + title, + // injected + intl, }) => ( - - {children} - + + + +
+
{title}
+ {subtitle} +
+
+ {isError && } + +
+
+ +
{title}
+
+ +
+
+
+ + {children} + +
); -CollapsibleFormWidget.propTypes = { - title: PropTypes.node.isRequired, - children: PropTypes.node.isRequired, + +CollapsibleFormWidget.defaultProps = { + subtitle: null, }; -export default CollapsibleFormWidget; +CollapsibleFormWidget.propTypes = { + children: PropTypes.node.isRequired, + isError: PropTypes.bool.isRequired, + subtitle: PropTypes.node, + title: PropTypes.node.isRequired, + // injected + intl: intlShape.isRequired, +}; + +export default injectIntl(CollapsibleFormWidget); diff --git a/src/editors/containers/VideoEditor/components/VideoSettingsModal/components/CollapsibleFormWidget.test.jsx b/src/editors/containers/VideoEditor/components/VideoSettingsModal/components/CollapsibleFormWidget.test.jsx new file mode 100644 index 000000000..9ef919d27 --- /dev/null +++ b/src/editors/containers/VideoEditor/components/VideoSettingsModal/components/CollapsibleFormWidget.test.jsx @@ -0,0 +1,28 @@ +import React from 'react'; +import { shallow } from 'enzyme'; + +import { formatMessage } from '../../../../../../testUtils'; +import { CollapsibleFormWidget } from './CollapsibleFormWidget'; + +describe('CollapsibleFormWidget', () => { + const props = { + isError: false, + subtitle: 'SuBTItle', + title: 'tiTLE', + // inject + intl: { formatMessage }, + }; + describe('render', () => { + const testContent = (

Some test string

); + test('snapshots: renders as expected with default props', () => { + expect( + shallow({testContent}), + ).toMatchSnapshot(); + }); + test('snapshots: renders with open={true} when there is error', () => { + expect( + shallow({testContent}), + ).toMatchSnapshot(); + }); + }); +}); diff --git a/src/editors/containers/VideoEditor/components/VideoSettingsModal/components/ErrorSummary.jsx b/src/editors/containers/VideoEditor/components/VideoSettingsModal/components/ErrorSummary.jsx new file mode 100644 index 000000000..5b7ceeea0 --- /dev/null +++ b/src/editors/containers/VideoEditor/components/VideoSettingsModal/components/ErrorSummary.jsx @@ -0,0 +1,40 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { FormattedMessage } from '@edx/frontend-platform/i18n'; + +import { Alert } from '@edx/paragon'; +import { Info } from '@edx/paragon/icons'; +import messages from './messages'; + +export const ErrorSummary = ({ + error, +}) => ( + Object.keys(val).length === 0)} + variant="danger" + > + + + +

+ +

+
+); + +ErrorSummary.defaultProps = { + error: { + duration: {}, + handout: {}, + license: {}, + thumbnail: {}, + transcripts: {}, + videoSource: {}, + }, +}; +ErrorSummary.propTypes = { + error: PropTypes.node, +}; + +export default ErrorSummary; diff --git a/src/editors/containers/VideoEditor/components/VideoSettingsModal/components/ErrorSummary.test.jsx b/src/editors/containers/VideoEditor/components/VideoSettingsModal/components/ErrorSummary.test.jsx new file mode 100644 index 000000000..c0e985387 --- /dev/null +++ b/src/editors/containers/VideoEditor/components/VideoSettingsModal/components/ErrorSummary.test.jsx @@ -0,0 +1,17 @@ +import React from 'react'; +import { shallow } from 'enzyme'; + +import { ErrorSummary } from './ErrorSummary'; + +describe('ErrorSummary', () => { + const props = { + error: 'eRrOr', + }; + describe('render', () => { + test('snapshots: renders as expected', () => { + expect( + shallow(), + ).toMatchSnapshot(); + }); + }); +}); diff --git a/src/editors/containers/VideoEditor/components/VideoSettingsModal/components/TranscriptsWidget.jsx b/src/editors/containers/VideoEditor/components/VideoSettingsModal/components/TranscriptsWidget.jsx index a36de6d18..c893335c4 100644 --- a/src/editors/containers/VideoEditor/components/VideoSettingsModal/components/TranscriptsWidget.jsx +++ b/src/editors/containers/VideoEditor/components/VideoSettingsModal/components/TranscriptsWidget.jsx @@ -1,6 +1,6 @@ import React from 'react'; import { useDispatch } from 'react-redux'; -// import PropTypes from 'prop-types'; +import PropTypes from 'prop-types'; import hooks from './hooks'; import CollapsibleFormWidget from './CollapsibleFormWidget'; @@ -8,7 +8,9 @@ import CollapsibleFormWidget from './CollapsibleFormWidget'; /** * Collapsible Form widget controlling video transcripts */ -export const TranscriptWidget = () => { +export const TranscriptWidget = ({ + error, +}) => { const dispatch = useDispatch(); const values = hooks.widgetValues({ dispatch, @@ -23,8 +25,16 @@ export const TranscriptWidget = () => { allowTranscriptDownloads: allowDownload, showTranscriptByDefault: showByDefault, } = values; + + // TODO: replace the following sample subtitle input with one managed by hook logic + const sampleSubtitle =
{transcripts.formValue.english}
; + return ( - + Transcripts

English: {transcripts.formValue.english}

Allow downloads: {allowDownload.formValue ? 'True' : 'False' }

@@ -33,4 +43,11 @@ export const TranscriptWidget = () => { ); }; +TranscriptWidget.defaultProps = { + error: {}, +}; +TranscriptWidget.propTypes = { + error: PropTypes.node, +}; + export default TranscriptWidget; diff --git a/src/editors/containers/VideoEditor/components/VideoSettingsModal/components/VideoSourceWidget.jsx b/src/editors/containers/VideoEditor/components/VideoSettingsModal/components/VideoSourceWidget.jsx index 512e6551d..e38166dc7 100644 --- a/src/editors/containers/VideoEditor/components/VideoSettingsModal/components/VideoSourceWidget.jsx +++ b/src/editors/containers/VideoEditor/components/VideoSettingsModal/components/VideoSourceWidget.jsx @@ -34,7 +34,9 @@ export const VideoSourceWidget = () => { }); return ( - +
Video ID or URL diff --git a/src/editors/containers/VideoEditor/components/VideoSettingsModal/components/__snapshots__/CollapsibleFormWidget.test.jsx.snap b/src/editors/containers/VideoEditor/components/VideoSettingsModal/components/__snapshots__/CollapsibleFormWidget.test.jsx.snap new file mode 100644 index 000000000..2ea503592 --- /dev/null +++ b/src/editors/containers/VideoEditor/components/VideoSettingsModal/components/__snapshots__/CollapsibleFormWidget.test.jsx.snap @@ -0,0 +1,135 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`CollapsibleFormWidget render snapshots: renders as expected with default props 1`] = ` + + + +
+
+ tiTLE +
+ SuBTItle +
+
+ +
+
+ +
+ tiTLE +
+
+ +
+
+
+ +

+ Some test string +

+ +
+`; + +exports[`CollapsibleFormWidget render snapshots: renders with open={true} when there is error 1`] = ` + + + +
+
+ tiTLE +
+ SuBTItle +
+
+ + +
+
+ +
+ tiTLE +
+
+ +
+
+
+ +

+ Some test string +

+ +
+`; diff --git a/src/editors/containers/VideoEditor/components/VideoSettingsModal/components/__snapshots__/ErrorSummary.test.jsx.snap b/src/editors/containers/VideoEditor/components/VideoSettingsModal/components/__snapshots__/ErrorSummary.test.jsx.snap new file mode 100644 index 000000000..eb911b3f5 --- /dev/null +++ b/src/editors/containers/VideoEditor/components/VideoSettingsModal/components/__snapshots__/ErrorSummary.test.jsx.snap @@ -0,0 +1,23 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`ErrorSummary render snapshots: renders as expected 1`] = ` + + + + +

+ +

+
+`; diff --git a/src/editors/containers/VideoEditor/components/VideoSettingsModal/components/hooks.js b/src/editors/containers/VideoEditor/components/VideoSettingsModal/components/hooks.js index 6b961a4a0..725bbe1ae 100644 --- a/src/editors/containers/VideoEditor/components/VideoSettingsModal/components/hooks.js +++ b/src/editors/containers/VideoEditor/components/VideoSettingsModal/components/hooks.js @@ -205,18 +205,18 @@ export const arrayWidget = ({ dispatch, key }) => { const widget = module.valueHooks({ dispatch, key }); return { ...widget, - onChange: handleIndexTransformEvent({ - handler: onValue, - setter: widget.setLocal, - transform: module.updatedArray, - local: widget.local, - }), onBlur: handleIndexTransformEvent({ handler: onValue, setter: widget.setAll, transform: module.updatedArray, local: widget.local, }), + onChange: handleIndexTransformEvent({ + handler: onValue, + setter: widget.setLocal, + transform: module.updatedArray, + local: widget.local, + }), onClear: (index) => () => widget.setAll(module.updatedArray(widget.local, index, '')), }; }; diff --git a/src/editors/containers/VideoEditor/components/VideoSettingsModal/components/messages.js b/src/editors/containers/VideoEditor/components/VideoSettingsModal/components/messages.js new file mode 100644 index 000000000..a4e00f572 --- /dev/null +++ b/src/editors/containers/VideoEditor/components/VideoSettingsModal/components/messages.js @@ -0,0 +1,22 @@ +export const messages = { + expandAltText: { + id: 'authoring.videoeditor.expand', + defaultMessage: 'Expand', + }, + collapseAltText: { + id: 'authoring.videoeditor.collapse', + defaultMessage: 'Collapse', + }, + validateErrorTitle: { + id: 'authoring.videoeditor.validate.error.title', + defaultMessage: 'We couldn\'t add your video.', + description: 'Title of validation error.', + }, + validateErrorBody: { + id: 'authoring.videoeditor.validate.error.body', + defaultMessage: 'Please check your entries and try again.', + description: 'Body of validation error.', + }, +}; + +export default messages; diff --git a/src/editors/containers/VideoEditor/components/VideoSettingsModal/index.jsx b/src/editors/containers/VideoEditor/components/VideoSettingsModal/index.jsx index 5275b5376..70c3db680 100644 --- a/src/editors/containers/VideoEditor/components/VideoSettingsModal/index.jsx +++ b/src/editors/containers/VideoEditor/components/VideoSettingsModal/index.jsx @@ -1,7 +1,9 @@ import React from 'react'; +import PropTypes from 'prop-types'; import { thunkActions } from '../../../../data/redux'; // import VideoPreview from './components/VideoPreview'; +import ErrorSummary from './components/ErrorSummary'; import DurationWidget from './components/DurationWidget'; import HandoutWidget from './components/HandoutWidget'; import LicenseWidget from './components/LicenseWidget'; @@ -18,24 +20,39 @@ export const hooks = { }, }; -export const VideoSettingsModal = () => ( +export const VideoSettingsModal = ({ + error, +}) => (
Video Preview goes here {/* */}
- - - - - - + +

Settings

+ + + + + +
); +VideoSettingsModal.defaultProps = { + error: { + duration: {}, + handout: {}, + license: {}, + thumbnail: {}, + transcripts: {}, + videoSource: {}, + }, +}; VideoSettingsModal.propTypes = { + error: PropTypes.node, }; export default VideoSettingsModal; diff --git a/src/editors/containers/VideoEditor/hooks.js b/src/editors/containers/VideoEditor/hooks.js new file mode 100644 index 000000000..011b020b8 --- /dev/null +++ b/src/editors/containers/VideoEditor/hooks.js @@ -0,0 +1,80 @@ +import { useState } from 'react'; + +import { StrictDict } from '../../utils'; +import * as module from './hooks'; + +export const state = StrictDict({ + durationErrors: (val) => useState(val), + handoutErrors: (val) => useState(val), + licenseErrors: (val) => useState(val), + thumbnailErrors: (val) => useState(val), + transcriptsErrors: (val) => useState(val), + videoSourceErrors: (val) => useState(val), +}); + +export const errorsHook = () => { + const [durationErrors, setDurationErrors] = module.state.durationErrors({}); + const [handoutErrors, setHandoutErrors] = module.state.handoutErrors({}); + const [licenseErrors, setLicenseErrors] = module.state.licenseErrors({}); + const [thumbnailErrors, setThumbnailErrors] = module.state.thumbnailErrors({}); + const [transcriptsErrors, setTranscriptsErrors] = module.state.transcriptsErrors({}); + const [videoSourceErrors, setVideoSourceErrors] = module.state.videoSourceErrors({}); + + return { + error: { + duration: durationErrors, + handout: handoutErrors, + license: licenseErrors, + thumbnail: thumbnailErrors, + transcripts: transcriptsErrors, + videoSource: videoSourceErrors, + }, + validateEntry: () => { + let validated = true; + if (!module.validateDuration({ setDurationErrors })) { validated = false; } + if (!module.validateHandout({ setHandoutErrors })) { validated = false; } + if (!module.validateLicense({ setLicenseErrors })) { validated = false; } + if (!module.validateThumbnail({ setThumbnailErrors })) { validated = false; } + if (!module.validateTranscripts({ setTranscriptsErrors })) { validated = false; } + if (!module.validateVideoSource({ setVideoSourceErrors })) { validated = false; } + return validated; + }, + }; +}; + +export const validateDuration = ({ setDurationErrors }) => { + setDurationErrors({ + fieldName: 'sample error message', + }); + return false; +}; +export const validateHandout = ({ setHandoutErrors }) => { + setHandoutErrors({ + fieldName: 'sample error message', + }); + return false; +}; +export const validateLicense = ({ setLicenseErrors }) => { + setLicenseErrors({ + fieldName: 'sample error message', + }); + return false; +}; +export const validateThumbnail = ({ setThumbnailErrors }) => { + setThumbnailErrors({ + fieldName: 'sample error message', + }); + return false; +}; +export const validateTranscripts = ({ setTranscriptsErrors }) => { + setTranscriptsErrors({ + fieldName: 'sample error message', + }); + return false; +}; +export const validateVideoSource = ({ setVideoSourceErrors }) => { + setVideoSourceErrors({ + fieldName: 'sample error message', + }); + return false; +}; diff --git a/src/editors/containers/VideoEditor/hooks.test.js b/src/editors/containers/VideoEditor/hooks.test.js new file mode 100644 index 000000000..e12a09c6e --- /dev/null +++ b/src/editors/containers/VideoEditor/hooks.test.js @@ -0,0 +1,72 @@ +import { MockUseState } from '../../../testUtils'; + +import { keyStore } from '../../utils'; +import * as module from './hooks'; + +jest.mock('react', () => ({ + ...jest.requireActual('react'), +})); + +const state = new MockUseState(module); +const moduleKeys = keyStore(module); + +let hook; + +describe('VideoEditorHooks', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + describe('state hooks', () => { + state.testGetter(state.keys.durationErrors); + state.testGetter(state.keys.handoutErrors); + state.testGetter(state.keys.licenseErrors); + state.testGetter(state.keys.thumbnailErrors); + state.testGetter(state.keys.transcriptsErrors); + state.testGetter(state.keys.videoSourceErrors); + }); + + describe('errors hook', () => { + beforeEach(() => { + state.mock(); + }); + afterEach(() => { + state.restore(); + }); + + const mockTrue = () => true; + const mockFalse = () => false; + test('error: state values', () => { + expect(module.errorsHook().error).toEqual({ + duration: state.stateVals[state.keys.durationErrors], + handout: state.stateVals[state.keys.handoutErrors], + license: state.stateVals[state.keys.licenseErrors], + thumbnail: state.stateVals[state.keys.thumbnailErrors], + transcripts: state.stateVals[state.keys.transcriptsErrors], + videoSource: state.stateVals[state.keys.videoSourceErrors], + }); + }); + describe('validateEntry', () => { + beforeEach(() => { + hook = module.errorsHook(); + }); + test('validateEntry: returns true if all validation calls are true', () => { + jest.spyOn(module, moduleKeys.validateDuration).mockImplementationOnce(mockTrue); + jest.spyOn(module, moduleKeys.validateHandout).mockImplementationOnce(mockTrue); + jest.spyOn(module, moduleKeys.validateLicense).mockImplementationOnce(mockTrue); + jest.spyOn(module, moduleKeys.validateThumbnail).mockImplementationOnce(mockTrue); + jest.spyOn(module, moduleKeys.validateTranscripts).mockImplementationOnce(mockTrue); + jest.spyOn(module, moduleKeys.validateVideoSource).mockImplementationOnce(mockTrue); + expect(hook.validateEntry()).toEqual(true); + }); + test('validateEntry: returns false if any validation calls are false', () => { + jest.spyOn(module, moduleKeys.validateDuration).mockImplementationOnce(mockFalse); + jest.spyOn(module, moduleKeys.validateHandout).mockImplementationOnce(mockTrue); + jest.spyOn(module, moduleKeys.validateLicense).mockImplementationOnce(mockTrue); + jest.spyOn(module, moduleKeys.validateThumbnail).mockImplementationOnce(mockTrue); + jest.spyOn(module, moduleKeys.validateTranscripts).mockImplementationOnce(mockTrue); + jest.spyOn(module, moduleKeys.validateVideoSource).mockImplementationOnce(mockTrue); + expect(hook.validateEntry()).toEqual(false); + }); + }); + }); +}); diff --git a/src/editors/containers/VideoEditor/index.jsx b/src/editors/containers/VideoEditor/index.jsx index c15c48ca0..a7f6208d2 100644 --- a/src/editors/containers/VideoEditor/index.jsx +++ b/src/editors/containers/VideoEditor/index.jsx @@ -3,21 +3,29 @@ import PropTypes from 'prop-types'; import EditorContainer from '../EditorContainer'; import VideoEditorModal from './components/VideoEditorModal'; +import * as hooks from './hooks'; export default function VideoEditor({ onClose, }) { + const { + error, + validateEntry, + } = hooks.errorsHook(); + return ( ({})} + onClose={onClose} + validateEntry={validateEntry} >
- +
); } + VideoEditor.defaultProps = { onClose: null, }; diff --git a/src/editors/hooks.js b/src/editors/hooks.js index ceffe7efd..834e9cb48 100644 --- a/src/editors/hooks.js +++ b/src/editors/hooks.js @@ -29,17 +29,28 @@ export const navigateCallback = ({ export const nullMethod = () => ({}); export const saveBlock = ({ + analytics, content, destination, - analytics, dispatch, + validateEntry, }) => { - dispatch(thunkActions.app.saveBlock({ - returnToUnit: module.navigateCallback({ - destination, - analyticsEvent: analyticsEvt.editorSaveClick, - analytics, - }), - content, - })); + let attemptSave = false; + if (validateEntry) { + if (validateEntry()) { + attemptSave = true; + } + } else { + attemptSave = true; + } + if (attemptSave) { + dispatch(thunkActions.app.saveBlock({ + returnToUnit: module.navigateCallback({ + destination, + analyticsEvent: analyticsEvt.editorSaveClick, + analytics, + }), + content, + })); + } }; diff --git a/src/setupTest.js b/src/setupTest.js index 47d217d31..5d9521c41 100644 --- a/src/setupTest.js +++ b/src/setupTest.js @@ -67,6 +67,12 @@ jest.mock('@edx/paragon', () => jest.requireActual('testUtils').mockNestedCompon Spacer: 'ActionRow.Spacer', }, Button: 'Button', + Collapsible: { + Advanced: 'Advanced', + Body: 'Body', + Trigger: 'Trigger', + Visible: 'Visible', + }, Dropdown: { Item: 'Dropdown.Item', Menu: 'Dropdown.Menu',