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',