diff --git a/cms/djangoapps/contentstore/views/item.py b/cms/djangoapps/contentstore/views/item.py
index 399fc7055e0d..1cdc21a9104e 100644
--- a/cms/djangoapps/contentstore/views/item.py
+++ b/cms/djangoapps/contentstore/views/item.py
@@ -1,7 +1,5 @@
"""Views for items (modules)."""
-
-import hashlib
import logging
from collections import OrderedDict
from datetime import datetime
diff --git a/common/lib/xmodule/xmodule/video_module/static/.eslintrc.js b/common/lib/xmodule/xmodule/video_module/static/.eslintrc.js
new file mode 100644
index 000000000000..5af380794f91
--- /dev/null
+++ b/common/lib/xmodule/xmodule/video_module/static/.eslintrc.js
@@ -0,0 +1,14 @@
+module.exports = {
+ extends: 'eslint-config-edx',
+ root: true,
+ settings: {
+ 'import/resolver': {
+ webpack: {
+ config: 'webpack.dev.config.js',
+ },
+ },
+ },
+ rules: {
+ 'import/prefer-default-export': 'off',
+ },
+};
diff --git a/common/lib/xmodule/xmodule/video_module/static/VideoBlock/components/VideoBlockEditor.jsx b/common/lib/xmodule/xmodule/video_module/static/VideoBlock/components/VideoBlockEditor.jsx
new file mode 100644
index 000000000000..c2aad8fd3a3e
--- /dev/null
+++ b/common/lib/xmodule/xmodule/video_module/static/VideoBlock/components/VideoBlockEditor.jsx
@@ -0,0 +1,220 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import { fetchSettings, submitSettings, emit } from '../data/thunks';
+import { connect } from "react-redux";
+import Tabs from "@edx/paragon/src/Tabs";
+// // According to the docs for the version of Paragon installed, this should import versions of the components with
+// // style names that are scoped to paragon. This is true, but...
+// import {InputText, Button, CheckBox} from "@edx/paragon/static";
+// // ...The following import line fails from what appears to be a path issue.
+// import "@edx/paragon/static/paragon.min.css"
+// // So, we're falling back the base component imports.
+import {InputText, Button, CheckBox} from "@edx/paragon/src";
+import { VideoListContainer } from "./VideoList";
+import {SettingsShape} from "../data/shapes";
+
+
+export const VideoBlockEditor = ({changes, updateForm, saveSettings, errors}) => {
+ return (
+
+
+
+
+
+
+
+ { /* need to figure out way of handling error messages here. */}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ )
+};
+
+
+export const VideoBlockEditorContainerBase = ({
+ fetchSettings, submitSettings, settings, xblockElement, runtime, emit, changes, errors,
+}) => {
+ if (settings === null) {
+ fetchSettings({runtime, xblockElement}).catch((error) => {
+ if (runtime.notify) {
+ runtime.notify("error", {message: error + ''})
+ } else {
+ throw error
+ }
+ })
+ return ''
+ }
+ if (changes === null) {
+ // Can happen briefly after settings are first set in the store but before copied into the changes field.
+ return ''
+ }
+ const updateForm = (fieldName) => (newValue) => {
+ if (errors[fieldName]) {
+ const newErrors = {...errors}
+ delete newErrors[fieldName]
+ emit('setErrors', {errors: newErrors})
+ }
+ emit('updateChanges', {changes: {...changes, [fieldName]: newValue}})
+ }
+ const saveSettings = () => {
+ emit('setErrors', {errors: {}})
+ submitSettings({runtime, xblockElement, changes}).then((revised) => {
+ emit('updateSettings', {settings: revised})
+ emit('updateChanges', {changes: revised})
+ }).catch(() => undefined)
+ }
+ const fieldErrors = {}
+ // Usability standards suggest only showing one error message per field at a time.
+ // If there are multiple, just show one for now.
+ Object.keys(errors).map((key) => fieldErrors[key] = errors[key][0])
+ return
+};
+
+VideoBlockEditorContainerBase.propDefaults = {
+ changes: null,
+ settings: null,
+}
+
+VideoBlockEditorContainerBase.propTypes = {
+ fetchSettings: PropTypes.func.isRequired,
+ submitSettings: PropTypes.func.isRequired,
+ xblockElement: PropTypes.element.isRequired,
+ runtime: PropTypes.shape({
+ notify: PropTypes.func,
+ }),
+ emit: PropTypes.func.isRequired,
+ changes: SettingsShape,
+ settings: SettingsShape,
+}
+
+export const VideoBlockEditorContainer = connect(
+ (state) => ({settings: state.settings, changes: state.changes, errors: state.errors}),
+ {
+ fetchSettings,
+ submitSettings,
+ emit,
+ },
+)(VideoBlockEditorContainerBase)
+
diff --git a/common/lib/xmodule/xmodule/video_module/static/VideoBlock/components/VideoList.jsx b/common/lib/xmodule/xmodule/video_module/static/VideoBlock/components/VideoList.jsx
new file mode 100644
index 000000000000..f290e5c4e3c4
--- /dev/null
+++ b/common/lib/xmodule/xmodule/video_module/static/VideoBlock/components/VideoList.jsx
@@ -0,0 +1,91 @@
+import React, {useState} from "react";
+import PropTypes from 'prop-types';
+import {Button, InputText} from "@edx/paragon/src";
+
+/**
+ * VideoList template. See VideoListContainer below.
+ */
+export const VideoList = ({name, label, value, setExpanded, expanded, updatePosition}) => {
+ return (
+
+
+
+
+ {expanded && (
+
+
+ {gettext('To be sure all students can access the video, we recommend providing both an .mp4 and a .webm version of your video. Click below to add a URL for another version. These URLs cannot be YouTube URLs. The first listed video that\'s compatible with the student\'s computer will play.')}
+
+
+
+
+
+
+
+
+ )}
+
+ )
+}
+
+VideoList.propTypes = {
+ name: PropTypes.string.isRequired,
+ label: PropTypes.string.isRequired,
+ value: PropTypes.arrayOf(PropTypes.string).isRequired,
+ updatePosition: PropTypes.func.isRequired,
+ expanded: PropTypes.bool.isRequired,
+ setExpanded: PropTypes.func.isRequired,
+}
+
+/**
+ * VideoListContainer
+ * This function is a react component that handles the 'video_url' field, which, despite its name, is array of video
+ * urls, not just one. It's also dynamically constructed by the backend based on the set YoutubeID and the backup
+ * HTML5 sources.
+ *
+ * This component isn't complete-- it matches the functionality of the 'Basic' tab in studio, but either a different
+ * component needs to be made to handle the 'Advanced' tab's functionality or else this component needs to be
+ * refactored/split up.
+ */
+export const VideoListContainer = ({name, label, value, update}) => {
+ const updatePosition = (position) => (value) => {
+ const revised = [...value]
+ revised[position] = value;
+ update(revised);
+ }
+ const [expanded, setExpanded] = useState(false)
+ return (
+
+ )
+}
+
+VideoListContainer.propTypes = {
+ name: PropTypes.string.isRequired,
+ label: PropTypes.string.isRequired,
+ value: PropTypes.arrayOf(PropTypes.string).isRequired,
+ update: PropTypes.func.isRequired,
+}
diff --git a/common/lib/xmodule/xmodule/video_module/static/VideoBlock/data/api.js b/common/lib/xmodule/xmodule/video_module/static/VideoBlock/data/api.js
new file mode 100644
index 000000000000..e5ed88591719
--- /dev/null
+++ b/common/lib/xmodule/xmodule/video_module/static/VideoBlock/data/api.js
@@ -0,0 +1,32 @@
+import Cookies from "js-cookie";
+
+const HEADERS = {
+ Accept: 'application/json',
+ 'Content-Type': 'application/json',
+ 'X-CSRFToken': Cookies.get('csrftoken'),
+};
+
+export const getSettings = async ({ runtime, xblockElement }) => (
+ fetch(
+ runtime.handlerUrl(xblockElement, 'load_settings'),
+ {credentials: 'same-origin', method: 'get', headers: HEADERS},
+ ).then((response) => response.json())
+)
+
+export const postSettings = async ({ runtime, xblockElement, changes }) => (
+ fetch(
+ runtime.handlerUrl(xblockElement, 'save_settings'),
+ {credentials: 'same-origin', method: 'post', headers: HEADERS, body: JSON.stringify(changes)},
+ ).then(
+ async (response) => {
+ if ((response.status >= 300) || (response.status < 200)) {
+ const err = Error('API error.')
+ err.data = (
+ (await response.json()) || {'detail': gettext('We had trouble saving this block. Please try again later.')}
+ )
+ throw err
+ }
+ return await response.json()
+ },
+ )
+)
diff --git a/common/lib/xmodule/xmodule/video_module/static/VideoBlock/data/reducers.js b/common/lib/xmodule/xmodule/video_module/static/VideoBlock/data/reducers.js
new file mode 100644
index 000000000000..32483e2c1985
--- /dev/null
+++ b/common/lib/xmodule/xmodule/video_module/static/VideoBlock/data/reducers.js
@@ -0,0 +1,23 @@
+export const updateSettings = (state, {payload}) => {
+ return {...state, settings: payload.settings}
+};
+
+export const updateChanges = (state, {payload}) => {
+ return {...state, changes: payload.changes}
+}
+
+export const setErrors = (state, {payload}) => {
+ return {...state, errors: payload.errors}
+}
+
+export const genReducer = (reducingFunctions) => (state, action) => {
+ if (!action) {
+ return state
+ }
+ if (reducingFunctions[action.type]) {
+ state = reducingFunctions[action.type](state, action)
+ }
+ return state
+};
+
+export const rootReducer = genReducer({updateSettings, updateChanges, setErrors});
diff --git a/common/lib/xmodule/xmodule/video_module/static/VideoBlock/data/shapes.js b/common/lib/xmodule/xmodule/video_module/static/VideoBlock/data/shapes.js
new file mode 100644
index 000000000000..009417fc6ffa
--- /dev/null
+++ b/common/lib/xmodule/xmodule/video_module/static/VideoBlock/data/shapes.js
@@ -0,0 +1,14 @@
+import PropTypes from 'prop-types';
+
+export const SettingsShape = PropTypes.shape({
+ display_name: PropTypes.string.isRequired,
+ video_url: PropTypes.arrayOf(PropTypes.string),
+ video_id: PropTypes.string.isRequired,
+ only_on_web: PropTypes.bool.isRequired,
+ download_track: PropTypes.bool.isRequired,
+ download_video: PropTypes.bool.isRequired,
+ track: PropTypes.string.isRequired,
+ edx_video_id: PropTypes.string.isRequired,
+ start_time: PropTypes.string.isRequired,
+ end_time: PropTypes.string.isRequired,
+})
diff --git a/common/lib/xmodule/xmodule/video_module/static/VideoBlock/data/store.js b/common/lib/xmodule/xmodule/video_module/static/VideoBlock/data/store.js
new file mode 100644
index 000000000000..edf8f1a92f99
--- /dev/null
+++ b/common/lib/xmodule/xmodule/video_module/static/VideoBlock/data/store.js
@@ -0,0 +1,15 @@
+import { applyMiddleware, createStore } from 'redux';
+import thunkMiddleware from 'redux-thunk';
+
+import { rootReducer } from './reducers';
+
+const configureStore = (initialState) => createStore(
+ rootReducer,
+ initialState,
+ applyMiddleware(thunkMiddleware),
+);
+
+
+const store = configureStore({settings: null, changes: null, errors: {}});
+
+export default store;
diff --git a/common/lib/xmodule/xmodule/video_module/static/VideoBlock/data/thunks.js b/common/lib/xmodule/xmodule/video_module/static/VideoBlock/data/thunks.js
new file mode 100644
index 000000000000..dda5404bb843
--- /dev/null
+++ b/common/lib/xmodule/xmodule/video_module/static/VideoBlock/data/thunks.js
@@ -0,0 +1,38 @@
+import { getSettings, postSettings } from './api';
+
+export const fetchSettings = ({ runtime, xblockElement }) => async (dispatch) => {
+ const settings = await getSettings({ runtime, xblockElement }).catch((error) => {
+ throw error
+ });
+ dispatch({type: 'updateSettings', payload: {settings}})
+ dispatch({type: 'updateChanges', payload: {changes: settings}})
+}
+
+export const submitSettings = ({ runtime, xblockElement, changes }) => async (dispatch) => {
+ const notify = (label, data) => {
+ if (runtime.notify) {
+ runtime.notify(label, data)
+ }
+ }
+ notify('save', {state: 'start'})
+ return postSettings({runtime, xblockElement, changes}).then((settings) => {
+ dispatch({type: 'updateSettings', payload: {settings}})
+ notify('save', {state: 'end'});
+ return settings
+ }).catch((error) => {
+ let message = gettext('We had an error saving this component. Please check the form and try again.')
+ const errorData = error.data || {}
+ if (errorData.detail) {
+ message = error.detail
+ }
+ if (errorData.errors) {
+ dispatch({type: 'setErrors', payload: {errors: errorData.errors}})
+ }
+ notify('error', {title: gettext('Error Saving Video'), message: message});
+ throw error
+ })
+}
+
+export const emit = (type, data) => async (dispatch) => {
+ dispatch({type, payload: data})
+}
diff --git a/common/lib/xmodule/xmodule/video_module/static/VideoBlock/index.jsx b/common/lib/xmodule/xmodule/video_module/static/VideoBlock/index.jsx
new file mode 100644
index 000000000000..0aea25b38662
--- /dev/null
+++ b/common/lib/xmodule/xmodule/video_module/static/VideoBlock/index.jsx
@@ -0,0 +1,26 @@
+import React from 'react';
+import ReactDOM from 'react-dom';
+import { Provider } from 'react-redux';
+import store from './data/store';
+import { VideoBlockEditorContainer } from './components/VideoBlockEditor';
+
+window.videoBlockInit = (runtime, element) => {
+ ReactDOM.render(
+
+
+ ,
+ element,
+ );
+}
+
+// We're not able to import the full instrumentation of the micro front end libraries because they'll cause problems
+// with the installed JS depedencies here. These functions will exist in the CMS but may need to be provided in whatever
+// target runtime this code is executed in. For now, ensure these exist in some fashion. Question: Is i18n available
+// as a front-end XBlock runtime service? Should it be?
+if (!window.gettext) {
+ window.gettext = (val) => val
+}
+
+if (!window.ngettext) {
+ window.ngettext = (singular, plural, num) => (num === 1 ? singular : plural );
+}
diff --git a/common/lib/xmodule/xmodule/video_module/static/package.json b/common/lib/xmodule/xmodule/video_module/static/package.json
new file mode 100644
index 000000000000..e69de29bb2d1
diff --git a/common/lib/xmodule/xmodule/video_module/video_module.py b/common/lib/xmodule/xmodule/video_module/video_module.py
index 48c3dbbdd90c..f8da69770592 100644
--- a/common/lib/xmodule/xmodule/video_module/video_module.py
+++ b/common/lib/xmodule/xmodule/video_module/video_module.py
@@ -12,7 +12,7 @@
https://s3.amazonaws.com/edx-course-videos/edx-intro/edX-FA12-cware-1_100.webm
https://s3.amazonaws.com/edx-course-videos/edx-intro/edX-FA12-cware-1_100.ogv
"""
-
+from urllib.parse import urlparse, parse_qs
import copy
import json
@@ -25,7 +25,9 @@
from edx_django_utils.cache import RequestCache
from lxml import etree
from opaque_keys.edx.locator import AssetLocator
+from rest_framework import status
from web_fragments.fragment import Fragment
+from webob import Response
from xblock.completable import XBlockCompletionMode
from xblock.core import XBlock
from xblock.fields import ScopeIds
@@ -109,7 +111,7 @@
EXPORT_IMPORT_STATIC_DIR = u'static'
-@XBlock.wants('settings', 'completion', 'i18n', 'request_cache')
+@XBlock.wants('settings', 'completion', 'i18n', 'request_cache', 'transitional')
class VideoBlock(
VideoFields, VideoTranscriptsMixin, VideoStudioViewHandlers, VideoStudentViewHandlers,
TabsEditingMixin, EmptyDataRawMixin, XmlMixin, EditingMixin,
@@ -227,6 +229,73 @@ def prioritize_hls(self, youtube_streams, html5_sources):
return False
+ def get_settings(self):
+ """
+ Grabs the data that studio_view usually sends to the template rendering system and derives the fields
+ to display/edit from that.
+ """
+ context = self.get_context()
+ block_settings = {key: value['value'] for (key, value) in context['editable_metadata_fields'].items()}
+ block_settings.update({
+ key: value['value'] for (key, value) in context['transcripts_basic_tab_metadata'].items()
+ })
+ return block_settings
+
+ @XBlock.handler
+ def load_settings(self, data, suffix=''):
+ return Response(json=self.get_settings())
+
+ def set_video_values(self, video_urls):
+ """
+ This function takes 'video_url' (which is a list of URLs for videos for the block to load, depending on
+ compatibility) and determines what settings should be set on the block from it. We may not need this function
+ in the final version-- perhaps the data should be properly derived by the front end, and we POST
+ the target fields instead.
+ """
+ youtube_id_1_0 = ''
+ html5_sources = []
+ for url in video_urls:
+ parsed_url = urlparse(url)
+ if parsed_url.netloc in ['youtube.com', 'www.youtube.com']:
+ youtube_id_1_0 = parse_qs(parsed_url.query)['v'][0]
+ elif parsed_url.netloc == 'youtu.be':
+ youtube_id_1_0 = parsed_url.path.replace('/', '')
+ elif len(html5_sources) < 2:
+ html5_sources.append(url)
+ self.youtube_id_1_0 = youtube_id_1_0
+ self.html5_sources = html5_sources
+
+ @XBlock.json_handler
+ def save_settings(self, data, suffix=''):
+ """
+ Saves settings via AJAX request.
+ """
+ block_settings = self.get_settings()
+ errors = {}
+ for key, value in data.items():
+ if key in block_settings:
+ if hasattr(self, key):
+ # At least one field isn't real, which is video_url. There may be more.
+ try:
+ value = self.fields[key].from_json(value)
+ setattr(self, key, value)
+ except (ValueError, TypeError) as err:
+ # The errors provided by the serializer aren't always pretty, but they're usually useful.
+ # To see what this looks like, try putting a bogus value for 'end time'.
+ # This begs the questions if should make a Django REST serializer shim that this and other
+ # XBlocks can use much like Django models can use ModelSerializer.
+ # For now, we're passing these back to the client in the same format Django REST would,
+ # since we will probably standardize our error handling code to expect it at some point.
+ errors[key] = [str(err)]
+ block_settings[key] = value
+ # 'video_url' is a list.
+ self.set_video_values(video_urls=block_settings['video_url'])
+ # Special fields that were set need to be re-evaluated again.
+ block_settings = self.get_settings()
+ if errors:
+ return Response(json={'errors': errors}, status=status.HTTP_400_BAD_REQUEST)
+ return Response(json=block_settings)
+
def student_view(self, _context):
"""
Return the student view.
@@ -242,7 +311,7 @@ def author_view(self, context):
"""
return self.student_view(context)
- def studio_view(self, _context):
+ def legacy_editor(self, _context):
"""
Return the studio view.
"""
@@ -253,6 +322,21 @@ def studio_view(self, _context):
shim_xmodule_js(fragment, 'TabsEditingDescriptor')
return fragment
+ def studio_view(self, _context):
+ """
+ Return the studio view.
+ """
+ transitional_service = self.runtime.service(self, 'transitional')
+ if transitional_service and transitional_service.load_new_editor_for_block(self):
+ fragment = Fragment(
+ ''
+ )
+ add_webpack_to_fragment(fragment, 'VideoBlockEditor')
+ fragment.initialize_js('videoBlockInit')
+ return fragment
+ # print('Returning old view.')
+ return self.legacy_editor(_context)
+
def public_view(self, context):
"""
Returns a fragment that contains the html for the public view
diff --git a/openedx/core/djangoapps/xblock/runtime/blockstore_runtime.py b/openedx/core/djangoapps/xblock/runtime/blockstore_runtime.py
index 2f474ebdd2ef..1e9f5b201379 100644
--- a/openedx/core/djangoapps/xblock/runtime/blockstore_runtime.py
+++ b/openedx/core/djangoapps/xblock/runtime/blockstore_runtime.py
@@ -11,6 +11,7 @@
from xblock.exceptions import NoSuchDefinition, NoSuchUsage
from xblock.fields import ScopeIds
+from openedx.core.djangoapps.xblock.runtime.transitional_service import TransitionalService
from openedx.core.djangoapps.xblock.learning_context.manager import get_learning_context_impl
from openedx.core.djangoapps.xblock.runtime.runtime import XBlockRuntime
from openedx.core.djangoapps.xblock.runtime.olx_parsing import parse_xblock_include, BundleFormatException
@@ -33,6 +34,14 @@ class BlockstoreXBlockRuntime(XBlockRuntime):
def parse_xml_file(self, fileobj, id_generator=None):
raise NotImplementedError("Use parse_olx_file() instead")
+ def service(self, block, service_name):
+ """
+ Adding in a hacky shim to make sure we can load the TransitionalService. Is there a better place to inject it?
+ """
+ if service_name == 'transitional':
+ return TransitionalService()
+ return super(BlockstoreXBlockRuntime, self).service(block, service_name)
+
def get_block(self, usage_id, for_parent=None):
"""
Create an XBlock instance in this runtime.
diff --git a/openedx/core/djangoapps/xblock/runtime/transitional_service.py b/openedx/core/djangoapps/xblock/runtime/transitional_service.py
new file mode 100644
index 000000000000..322b4be8acca
--- /dev/null
+++ b/openedx/core/djangoapps/xblock/runtime/transitional_service.py
@@ -0,0 +1,12 @@
+class TransitionalService(object):
+ """
+ An XBlock service to be used for flagging that the runtime environment supports transitional
+ features, not yet finalized.
+
+ This will primarily be used for the Video and Problem blocks in order for them to know they can safely
+ run their experimental studio views compatible with the new Micro Front Ends.
+ """
+
+ def load_new_editor_for_block(self, _xblock):
+ # Right now we're only loading this service in Blockstore, so it should always be true.
+ return True
diff --git a/package-lock.json b/package-lock.json
index ea3dd15690e0..0953eafe9d33 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -11169,14 +11169,48 @@
"integrity": "sha1-eHTi04kR0nGeoncx0yRF2l7EOUw="
},
"react": {
- "version": "16.1.0",
- "resolved": "https://registry.npmjs.org/react/-/react-16.1.0.tgz",
- "integrity": "sha512-hvKYlKqde2JNnNiEzORvSA0J1L7uSZ43l+J89ZNoP4EXxQrVNH0CFj8vorfPou3w+1ou1BNMBir2VVsuXtETRA==",
+ "version": "16.13.1",
+ "resolved": "https://registry.npmjs.org/react/-/react-16.13.1.tgz",
+ "integrity": "sha512-YMZQQq32xHLX0bz5Mnibv1/LHb3Sqzngu7xstSM+vrkE5Kzr9xE0yMByK5kMoTK30YVJE61WfbxIFFvfeDKT1w==",
"requires": {
- "fbjs": "^0.8.16",
"loose-envify": "^1.1.0",
"object-assign": "^4.1.1",
- "prop-types": "^15.6.0"
+ "prop-types": "^15.6.2"
+ },
+ "dependencies": {
+ "prop-types": {
+ "version": "15.7.2",
+ "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.7.2.tgz",
+ "integrity": "sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ==",
+ "requires": {
+ "loose-envify": "^1.4.0",
+ "object-assign": "^4.1.1",
+ "react-is": "^16.8.1"
+ },
+ "dependencies": {
+ "loose-envify": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
+ "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
+ "requires": {
+ "js-tokens": "^3.0.0 || ^4.0.0"
+ }
+ }
+ }
+ },
+ "react-is": {
+ "version": "16.13.1",
+ "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
+ "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="
+ }
+ }
+ },
+ "react-clientside-effect": {
+ "version": "1.2.2",
+ "resolved": "https://registry.npmjs.org/react-clientside-effect/-/react-clientside-effect-1.2.2.tgz",
+ "integrity": "sha512-nRmoyxeok5PBO6ytPvSjKp9xwXg9xagoTK1mMjwnQxqM9Hd7MNPl+LS1bOSOe+CV2+4fnEquc7H/S8QD3q697A==",
+ "requires": {
+ "@babel/runtime": "^7.0.0"
}
},
"react-clientside-effect": {
@@ -11188,14 +11222,50 @@
}
},
"react-dom": {
- "version": "16.1.2",
- "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-16.1.2.tgz",
- "integrity": "sha512-e2iKeys/pJcIzszO+9EYxnTus6JI3pftzhXGG8K9B9co2SljknimfuY1/VlAG9nDBipDAc8jkIuuhlz+EiGL+g==",
+ "version": "16.13.1",
+ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-16.13.1.tgz",
+ "integrity": "sha512-81PIMmVLnCNLO/fFOQxdQkvEq/+Hfpv24XNJfpyZhTRfO0QcmQIF/PgCa1zCOj2w1hrn12MFLyaJ/G0+Mxtfag==",
"requires": {
- "fbjs": "^0.8.16",
"loose-envify": "^1.1.0",
"object-assign": "^4.1.1",
- "prop-types": "^15.6.0"
+ "prop-types": "^15.6.2",
+ "scheduler": "^0.19.1"
+ },
+ "dependencies": {
+ "prop-types": {
+ "version": "15.7.2",
+ "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.7.2.tgz",
+ "integrity": "sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ==",
+ "requires": {
+ "loose-envify": "^1.4.0",
+ "object-assign": "^4.1.1",
+ "react-is": "^16.8.1"
+ },
+ "dependencies": {
+ "loose-envify": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
+ "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
+ "requires": {
+ "js-tokens": "^3.0.0 || ^4.0.0"
+ }
+ }
+ }
+ },
+ "react-is": {
+ "version": "16.13.1",
+ "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
+ "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="
+ },
+ "scheduler": {
+ "version": "0.19.1",
+ "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.19.1.tgz",
+ "integrity": "sha512-n/zwRWRYSUj0/3g/otKDRPMh6qv2SYMWNq85IEa8iZyAv8od9zDYpGSnpBEjNgcMNq6Scbu5KfIPxNF72R/2EA==",
+ "requires": {
+ "loose-envify": "^1.1.0",
+ "object-assign": "^4.1.1"
+ }
+ }
}
},
"react-dropzone": {
diff --git a/package.json b/package.json
index 3c8a392922a2..2cdee31f5f50 100644
--- a/package.json
+++ b/package.json
@@ -45,8 +45,8 @@
"popper.js": "1.12.9",
"prop-types": "15.6.0",
"raw-loader": "0.5.1",
- "react": "16.1.0",
- "react-dom": "16.1.2",
+ "react": "16.13.1",
+ "react-dom": "16.13.1",
"react-focus-lock": "^1.19.1",
"react-redux": "5.0.7",
"react-router-dom": "5.1.2",
diff --git a/webpack.common.config.js b/webpack.common.config.js
index dd4111065f21..5f8a4de4092d 100644
--- a/webpack.common.config.js
+++ b/webpack.common.config.js
@@ -79,6 +79,7 @@ module.exports = Merge.smart({
'js/factories/xblock_validation': './cms/static/js/factories/xblock_validation.js',
'js/factories/edit_tabs': './cms/static/js/factories/edit_tabs.js',
'js/sock': './cms/static/js/sock.js',
+ VideoBlockEditor: './common/lib/xmodule/xmodule/video_module/static/VideoBlock/index.jsx',
// LMS
SingleSupportForm: './lms/static/support/jsx/single_support_form.jsx',