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
+
+
+
+
+
+
+
+
+ Some test string
+
+
+
+`;
+
+exports[`CollapsibleFormWidget render snapshots: renders with open={true} when there is error 1`] = `
+
+
+
+
+
+
+
+
+
+
+
+ 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',