diff --git a/src/courseware/course/Course.jsx b/src/courseware/course/Course.jsx index 3cb98285..6e004576 100644 --- a/src/courseware/course/Course.jsx +++ b/src/courseware/course/Course.jsx @@ -105,6 +105,7 @@ function Course({ { + sequenceIds = sequenceIds.concat(curSection.sequenceIds); + }); + } + const sequences = useModels('sequences', sequenceIds); + let unitIds = []; + if (sequences) { + sequences.forEach((curSequence) => { + unitIds = unitIds.concat(curSequence.unitIds); + }); + } + const units = useModels('units', unitIds); + + const [pluginUrl, setPluginUrl] = useState(''); + const handlePluginUrlChange = (event) => { + setPluginUrl(event.target.value); + }; + + const handlePluginAdd = (event) => { + event.preventDefault(); + loadDynamicScript(pluginUrl).then(() => { + const pluginFunction = loadScriptComponent('plugin', './Pomodoro'); + pluginFunction(); + setPluginUrl(''); + }); + }; + + if (!course || !section || !unit || !sequence) { + return null; + } + return ( +
+
+
+

Add a plugin?

+ + + +
+
+ {getConfig().plugins.slots.coursewareSidebar.map((plugin) => ( + + +
+
+ ))} +
+ ); +} + +InContextSidebar.propTypes = { + courseId: PropTypes.string, + sectionId: PropTypes.string, + unitId: PropTypes.string, + sequenceId: PropTypes.string, +}; + +InContextSidebar.defaultProps = { + courseId: null, + sectionId: null, + unitId: null, + sequenceId: null, +}; diff --git a/src/courseware/course/sequence/Sequence.jsx b/src/courseware/course/sequence/Sequence.jsx index 75ab27d8..4990fe5b 100644 --- a/src/courseware/course/sequence/Sequence.jsx +++ b/src/courseware/course/sequence/Sequence.jsx @@ -15,6 +15,7 @@ import { history } from '@edx/frontend-platform'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { faChevronLeft } from '@fortawesome/free-solid-svg-icons'; +import { Col, Container, Row } from '@edx/paragon'; import PageLoading from '../../../generic/PageLoading'; import { UserMessagesContext, ALERT_TYPES } from '../../../generic/user-messages'; import { useModel } from '../../../generic/model-store'; @@ -23,6 +24,7 @@ import CourseLicense from '../course-license'; import messages from './messages'; import { SequenceNavigation, UnitNavigation } from './sequence-navigation'; import SequenceContent from './SequenceContent'; +import InContextSidebar from './InContextSidebar'; function REV1512Flyover({ toggleREV1512Flyover }) { // This component should be reverted after the REV1512 experiment @@ -183,6 +185,7 @@ REV1512FlyoverMobile.propTypes = { function Sequence({ unitId, + sectionId, sequenceId, courseId, unitNavigationHandler, @@ -309,61 +312,74 @@ function Sequence({ if (sequenceStatus === 'loaded') { return (
-
-
- { - logEvent('edx.ui.lms.sequence.next_selected', 'top'); - handleNext(); - }} - onNavigate={(destinationUnitId) => { - logEvent('edx.ui.lms.sequence.tab_selected', 'top', destinationUnitId); - handleNavigate(destinationUnitId); - }} - previousSequenceHandler={() => { - logEvent('edx.ui.lms.sequence.previous_selected', 'top'); - handlePrevious(); - }} - goToCourseExitPage={() => goToCourseExitPage()} - /> -
- + + + - {unitHasLoaded && ( - { - logEvent('edx.ui.lms.sequence.previous_selected', 'bottom'); - handlePrevious(); - }} - onClickNext={() => { - logEvent('edx.ui.lms.sequence.next_selected', 'bottom'); + className="mb-4" + /* This line should be reverted after REV1512 experiment */ + toggleREV1512Flyover={toggleREV1512Flyover} + /* This line should be reverted after REV1512 experiment */ + REV1512FlyoverEnabled={REV1512FlyoverEnabled} + /* should be reverted after REV1512 experiment */ + isREV1512FlyoverVisible={isREV1512FlyoverVisible} + nextSequenceHandler={() => { + logEvent('edx.ui.lms.sequence.next_selected', 'top'); handleNext(); }} + onNavigate={(destinationUnitId) => { + logEvent('edx.ui.lms.sequence.tab_selected', 'top', destinationUnitId); + handleNavigate(destinationUnitId); + }} + previousSequenceHandler={() => { + logEvent('edx.ui.lms.sequence.previous_selected', 'top'); + handlePrevious(); + }} goToCourseExitPage={() => goToCourseExitPage()} /> - )} -
-
- {/* This block of code should be reverted post REV1512 experiment */} - {REV1512FlyoverEnabled && isREV1512FlyoverVisible() && ( - isMobile - ? - : - )} -
+
+ + {unitHasLoaded && ( + { + logEvent('edx.ui.lms.sequence.previous_selected', 'bottom'); + handlePrevious(); + }} + onClickNext={() => { + logEvent('edx.ui.lms.sequence.next_selected', 'bottom'); + handleNext(); + }} + goToCourseExitPage={() => goToCourseExitPage()} + /> + )} +
+ + + + + {/* This block of code should be reverted post REV1512 experiment */} + {REV1512FlyoverEnabled && isREV1512FlyoverVisible() && ( + isMobile + ? + : + )} + +
); @@ -380,6 +396,7 @@ function Sequence({ Sequence.propTypes = { unitId: PropTypes.string, sequenceId: PropTypes.string, + sectionId: PropTypes.string, courseId: PropTypes.string.isRequired, unitNavigationHandler: PropTypes.func.isRequired, nextSequenceHandler: PropTypes.func.isRequired, @@ -392,6 +409,7 @@ Sequence.propTypes = { Sequence.defaultProps = { sequenceId: null, + sectionId: null, unitId: null, }; diff --git a/src/index.jsx b/src/index.jsx index 9dc01be5..a457b2a2 100755 --- a/src/index.jsx +++ b/src/index.jsx @@ -4,6 +4,7 @@ import 'regenerator-runtime/runtime'; import { APP_INIT_ERROR, APP_READY, subscribe, initialize, mergeConfig, + getConfig, } from '@edx/frontend-platform'; import { AppProvider, ErrorPage, PageRoute } from '@edx/frontend-platform/react'; import React from 'react'; @@ -28,6 +29,8 @@ import { fetchDatesTab, fetchOutlineTab, fetchProgressTab } from './course-home/ import { fetchCourse } from './courseware/data'; import initializeStore from './store'; import PluginTestPage from './plugin-test/PluginTestPage'; +import { COMPONENT } from './plugin-test/PluginComponent'; +import { loadDynamicScript, loadScriptComponent } from './plugin-test/utils'; subscribe(APP_READY, () => { ReactDOM.render( @@ -70,6 +73,13 @@ subscribe(APP_READY, () => { , document.getElementById('root'), ); + + getConfig().plugins.scripts.forEach((plugin) => { + loadDynamicScript(plugin.url).then(() => { + const pluginFunction = loadScriptComponent(plugin.scope, plugin.module); + pluginFunction(); + }); + }); }); subscribe(APP_INIT_ERROR, (error) => { @@ -92,6 +102,43 @@ initialize({ SUPPORT_URL_VERIFIED_CERTIFICATE: process.env.SUPPORT_URL_VERIFIED_CERTIFICATE || null, TWITTER_HASHTAG: process.env.TWITTER_HASHTAG || null, TWITTER_URL: process.env.TWITTER_URL || null, + plugins: { + scripts: [ + // { + // url: 'http://localhost:7331/remoteEntry.js', + // scope: 'plugin', + // module: './Pomodoro', + // type: SCRIPT, + // }, + ], + slots: { + header: [ + { + url: 'http://localhost:7331/remoteEntry.js', + scope: 'plugin', + module: './Banner', + type: COMPONENT, + }, + ], + coursewareSidebar: [ + { + url: 'http://localhost:7331/remoteEntry.js', + scope: 'plugin', + module: './Discussions', + type: COMPONENT, + }, + { + url: 'http://localhost:7331/remoteEntry.js', + scope: 'plugin', + module: './Calendly', + type: COMPONENT, + props: { + username: 'djoy', + }, + }, + ], + }, + }, }, 'LearnerAppConfig'); }, }, diff --git a/src/index.scss b/src/index.scss index 1b5902a4..cd2adb61 100755 --- a/src/index.scss +++ b/src/index.scss @@ -95,8 +95,6 @@ } .sequence-container { - display: flex; - flex-direction: column; flex-grow: 1; margin-bottom: 4rem; @@ -111,6 +109,7 @@ } .sequence { + width: 100%; @media (min-width: map-get($grid-breakpoints, 'sm')) { border: solid 1px #EAEAEA; border-radius: 4px; @@ -302,9 +301,9 @@ .unit-iframe-wrapper { margin: 0 -20px 2rem; - @media (min-width: 830px) { - margin: 0 -40px 2rem; - } + // @media (min-width: 830px) { + // margin: 0 -20px 2rem; + // } } #unit-iframe { diff --git a/src/plugin-test/Plugin.jsx b/src/plugin-test/PluginComponent.jsx similarity index 77% rename from src/plugin-test/Plugin.jsx rename to src/plugin-test/PluginComponent.jsx index c1141f88..ac64e781 100644 --- a/src/plugin-test/Plugin.jsx +++ b/src/plugin-test/PluginComponent.jsx @@ -5,6 +5,7 @@ import PageLoading from '../generic/PageLoading'; import messages from './messages'; import { loadDynamicScript, loadScriptComponent } from './utils'; +import PluginErrorBoundary from './PluginErrorBoundary'; // These are intended to represent three different plugin types. They're not fully used yet. // Different plugins of different types would have different loading functionality. @@ -16,6 +17,7 @@ export const LTI = 'lti'; // loads LTI iframe at the URL, rather than loading a const useDynamicScript = (url) => { const [ready, setReady] = React.useState(false); const [failed, setFailed] = React.useState(false); + // eslint-disable-next-line no-unused-vars const [element, setElement] = React.useState(null); React.useEffect(() => { if (!url) { @@ -34,7 +36,9 @@ const useDynamicScript = (url) => { }); return () => { - document.head.removeChild(element); + // TODO: How to do reference checking on this script to only remove it when the last + // component using it has unmounted? + // document.head.removeChild(element); }; }, [url]); @@ -44,7 +48,7 @@ const useDynamicScript = (url) => { }; }; -function Plugin({ plugin, intl }) { +function PluginComponent({ plugin, intl, ...props }) { const url = plugin ? plugin.url : null; const { ready, failed } = useDynamicScript(url); @@ -64,7 +68,7 @@ function Plugin({ plugin, intl }) { return null; } - const PluginComponent = React.lazy( + const Component = React.lazy( loadScriptComponent(plugin.scope, plugin.module), ); @@ -76,23 +80,26 @@ function Plugin({ plugin, intl }) { /> )} > - + + + ); } -Plugin.propTypes = { +PluginComponent.propTypes = { plugin: PropTypes.shape({ scope: PropTypes.string.isRequired, module: PropTypes.string.isRequired, url: PropTypes.string.isRequired, type: PropTypes.oneOf([COMPONENT, SCRIPT, IFRAME]).isRequired, + props: PropTypes.object, }), intl: intlShape.isRequired, }; -Plugin.defaultProps = { +PluginComponent.defaultProps = { plugin: null, }; -export default injectIntl(Plugin); +export default injectIntl(PluginComponent); diff --git a/src/plugin-test/PluginErrorBoundary.jsx b/src/plugin-test/PluginErrorBoundary.jsx new file mode 100644 index 00000000..0136537b --- /dev/null +++ b/src/plugin-test/PluginErrorBoundary.jsx @@ -0,0 +1,36 @@ +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; + +export default class PluginErrorBoundary extends Component { + constructor(props) { + super(props); + this.state = { hasError: false }; + } + + static getDerivedStateFromError() { + // Update state so the next render will show the fallback UI. + return { hasError: true }; + } + + componentDidCatch(error, errorInfo) { + // eslint-disable-next-line no-console + console.error(error, errorInfo); + } + + render() { + if (this.state.hasError) { + // You can render any custom fallback UI + return
Plugin failed to load.
; + } + + return this.props.children; + } +} + +PluginErrorBoundary.propTypes = { + children: PropTypes.node, +}; + +PluginErrorBoundary.defaultProps = { + children: null, +}; diff --git a/src/plugin-test/PluginTestPage.jsx b/src/plugin-test/PluginTestPage.jsx index 614afc00..46f19b78 100644 --- a/src/plugin-test/PluginTestPage.jsx +++ b/src/plugin-test/PluginTestPage.jsx @@ -1,30 +1,9 @@ import React from 'react'; -import { Button } from '@edx/paragon'; - -import Plugin, { SCRIPT, COMPONENT } from './Plugin'; +import { getConfig } from '@edx/frontend-platform'; +import PluginComponent from './PluginComponent'; function PluginTestPage() { - const [plugin, setPlugin] = React.useState(undefined); - - function setPluginOne() { - setPlugin({ - url: 'http://localhost:7331/remoteEntry.js', - scope: 'plugin', - module: './PluginOne', - type: SCRIPT, - }); - } - - function setPluginTwo() { - setPlugin({ - url: 'http://localhost:7331/remoteEntry.js', - scope: 'plugin', - module: './PluginTwo', - type: COMPONENT, - }); - } - return (

Dynamic Plugin Host

@@ -34,10 +13,15 @@ function PluginTestPage() { remotes and exposes. It will no load components that have been loaded already.

- - + {/* + */}
- + {getConfig().plugins.slots.testPage.map((plugin) => ( + + ))}
); diff --git a/src/tab-page/LoadedTabPage.jsx b/src/tab-page/LoadedTabPage.jsx index b7cb9288..5f5f062c 100644 --- a/src/tab-page/LoadedTabPage.jsx +++ b/src/tab-page/LoadedTabPage.jsx @@ -10,6 +10,7 @@ import { AlertList } from '../generic/user-messages'; import InstructorToolbar from '../instructor-toolbar'; import useEnrollmentAlert from '../alerts/enrollment-alert'; import useLogistrationAlert from '../alerts/logistration-alert'; +import PluginComponent from '../plugin-test/PluginComponent'; function LoadedTabPage({ activeTabSlug, @@ -38,6 +39,12 @@ function LoadedTabPage({ {`${activeTab.title} | ${title} | ${getConfig().SITE_NAME}`} + {getConfig().plugins.slots.header.map((plugin) => ( + + ))}
)} +