Compare commits

..

1 Commits

Author SHA1 Message Date
David Joy
cddde17a0d Ready for a demo! Has an in-context sidebar. 2021-02-11 14:46:56 -05:00
19 changed files with 313 additions and 252 deletions

1
.env
View File

@@ -29,4 +29,3 @@ SUPPORT_URL_VERIFIED_CERTIFICATE=null
TWITTER_HASHTAG=null
TWITTER_URL=null
USER_INFO_COOKIE_NAME=null
NEXBLOCK_API_PATH=null

View File

@@ -29,5 +29,3 @@ SUPPORT_URL_VERIFIED_CERTIFICATE='https://support.edx.org/hc/en-us/articles/2065
TWITTER_HASHTAG='myedxjourney'
TWITTER_URL='https://twitter.com/edXOnline'
USER_INFO_COOKIE_NAME='edx-user-info'
NEXBLOCK_INSTANCE_DATA_API_PATH=http://localhost:18000/api/nexblock/v0/instance-data
NEXBLOCK_LEARNER_DATA_API_PATH=http://localhost:18000/api/nexblock/v0/learner-data

View File

@@ -105,6 +105,7 @@ function Course({
<AlertList topic="sequence" />
<Sequence
unitId={unitId}
sectionId={section ? section.id : null}
sequenceId={sequenceId}
courseId={courseId}
unitNavigationHandler={unitNavigationHandler}

View File

@@ -0,0 +1,96 @@
import React, { useState } from 'react';
import PropTypes from 'prop-types';
import { Button, Form } from '@edx/paragon';
import { getConfig } from '@edx/frontend-platform';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faPlug } from '@fortawesome/free-solid-svg-icons';
import PluginComponent from '../../../plugin-test/PluginComponent';
import { useModel, useModels } from '../../../generic/model-store';
import { loadDynamicScript, loadScriptComponent } from '../../../plugin-test/utils';
export default function InContextSidebar({
courseId, sectionId, unitId, sequenceId,
}) {
const course = useModel('coursewareMeta', courseId);
const section = useModel('sections', sectionId);
const unit = useModel('units', unitId);
const sequence = useModel('sequences', sequenceId);
const sections = useModels('sections', course ? course.sectionIds : []);
let sequenceIds = [];
if (sections) {
sections.forEach((curSection) => {
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 (
<div className="pt-3">
<div className="mb-3">
<Form onSubmit={handlePluginAdd}>
<h4><FontAwesomeIcon icon={faPlug} /> Add a plugin?</h4>
<Form.Control className="mb-1" onChange={handlePluginUrlChange} placeholder="URL to your plugin..." type="text" />
<Button type="submit">Add</Button>
</Form>
</div>
<hr />
{getConfig().plugins.slots.coursewareSidebar.map((plugin) => (
<React.Fragment key={`plugin-${plugin.url}-${plugin.module}`}>
<PluginComponent
course={course}
activeUnit={unit}
activeSequence={sequence}
activeSection={section}
unitIds={unitIds}
units={units}
sequences={sequences}
sections={sections}
plugin={plugin}
/>
<hr />
</React.Fragment>
))}
</div>
);
}
InContextSidebar.propTypes = {
courseId: PropTypes.string,
sectionId: PropTypes.string,
unitId: PropTypes.string,
sequenceId: PropTypes.string,
};
InContextSidebar.defaultProps = {
courseId: null,
sectionId: null,
unitId: null,
sequenceId: null,
};

View File

@@ -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 (
<div>
<div className="sequence-container" style={{ display: 'inline-flex', flexDirection: 'row' }}>
<div className="sequence" style={{ width: '100%' }}>
<SequenceNavigation
sequenceId={sequenceId}
unitId={unitId}
className="mb-4"
toggleREV1512Flyover={toggleREV1512Flyover} /* This line should be reverted after REV1512 experiment */
REV1512FlyoverEnabled={REV1512FlyoverEnabled} /* This line should be reverted after REV1512 experiment */
isREV1512FlyoverVisible={isREV1512FlyoverVisible} /* should be reverted after REV1512 experiment */
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()}
/>
<div className="unit-container flex-grow-1">
<SequenceContent
courseId={courseId}
gated={gated}
<Container className="sequence-container px-3">
<Row>
<Col sm={8} xs={12} className="sequence px-0">
<SequenceNavigation
sequenceId={sequenceId}
unitId={unitId}
unitLoadedHandler={handleUnitLoaded}
/>
{unitHasLoaded && (
<UnitNavigation
sequenceId={sequenceId}
unitId={unitId}
onClickPrevious={() => {
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()}
/>
)}
</div>
</div>
{/* This block of code should be reverted post REV1512 experiment */}
{REV1512FlyoverEnabled && isREV1512FlyoverVisible() && (
isMobile
? <REV1512FlyoverMobile toggleREV1512Flyover={toggleREV1512Flyover} />
: <REV1512Flyover toggleREV1512Flyover={toggleREV1512Flyover} />
)}
</div>
<div className="unit-container flex-grow-1">
<SequenceContent
courseId={courseId}
gated={gated}
sequenceId={sequenceId}
unitId={unitId}
unitLoadedHandler={handleUnitLoaded}
/>
{unitHasLoaded && (
<UnitNavigation
sequenceId={sequenceId}
unitId={unitId}
onClickPrevious={() => {
logEvent('edx.ui.lms.sequence.previous_selected', 'bottom');
handlePrevious();
}}
onClickNext={() => {
logEvent('edx.ui.lms.sequence.next_selected', 'bottom');
handleNext();
}}
goToCourseExitPage={() => goToCourseExitPage()}
/>
)}
</div>
</Col>
<Col className="ml-3 sequence">
<InContextSidebar
courseId={courseId}
sectionId={sectionId}
sequenceId={sequenceId}
unitId={unitId}
/>
</Col>
{/* This block of code should be reverted post REV1512 experiment */}
{REV1512FlyoverEnabled && isREV1512FlyoverVisible() && (
isMobile
? <REV1512FlyoverMobile toggleREV1512Flyover={toggleREV1512Flyover} />
: <REV1512Flyover toggleREV1512Flyover={toggleREV1512Flyover} />
)}
</Row>
</Container>
<CourseLicense license={course.license || undefined} />
</div>
);
@@ -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,
};

View File

@@ -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';
@@ -27,53 +28,58 @@ import { TabContainer } from './tab-page';
import { fetchDatesTab, fetchOutlineTab, fetchProgressTab } from './course-home/data';
import { fetchCourse } from './courseware/data';
import initializeStore from './store';
import NexBlockContainer from './nex-runtime/NexContainer';
import PluginTestPage from './plugin-test/PluginTestPage';
import { COMPONENT } from './plugin-test/PluginComponent';
import { loadDynamicScript, loadScriptComponent } from './plugin-test/utils';
subscribe(APP_READY, () => {
ReactDOM.render(
<AppProvider store={initializeStore()}>
<UserMessagesProvider>
<Switch>
<Route exact path="/nexblock" component={NexBlockContainer} />
<Switch>
<Route exact path="/plugin-test" component={PluginTestPage} />
<PageRoute path="/redirect" component={CoursewareRedirectLandingPage} />
<PageRoute path="/course/:courseId/home">
<TabContainer tab="outline" fetch={fetchOutlineTab} slice="courseHome">
<OutlineTab />
</TabContainer>
</PageRoute>
<PageRoute path="/course/:courseId/dates">
<TabContainer tab="dates" fetch={fetchDatesTab} slice="courseHome">
<DatesTab />
</TabContainer>
</PageRoute>
<PageRoute path="/course/:courseId/progress">
<TabContainer tab="progress" fetch={fetchProgressTab} slice="courseHome">
<ProgressTab />
</TabContainer>
</PageRoute>
<PageRoute path="/course/:courseId/course-end">
<TabContainer tab="courseware" fetch={fetchCourse} slice="courseware">
<CourseExit />
</TabContainer>
</PageRoute>
<PageRoute
path={[
'/course/:courseId/:sequenceId/:unitId',
'/course/:courseId/:sequenceId',
'/course/:courseId',
]}
component={CoursewareContainer}
/>
</Switch>
<Footer />
<Route exact path="/" component={PluginTestPage} />
<PageRoute path="/redirect" component={CoursewareRedirectLandingPage} />
<PageRoute path="/course/:courseId/home">
<TabContainer tab="outline" fetch={fetchOutlineTab} slice="courseHome">
<OutlineTab />
</TabContainer>
</PageRoute>
<PageRoute path="/course/:courseId/dates">
<TabContainer tab="dates" fetch={fetchDatesTab} slice="courseHome">
<DatesTab />
</TabContainer>
</PageRoute>
<PageRoute path="/course/:courseId/progress">
<TabContainer tab="progress" fetch={fetchProgressTab} slice="courseHome">
<ProgressTab />
</TabContainer>
</PageRoute>
<PageRoute path="/course/:courseId/course-end">
<TabContainer tab="courseware" fetch={fetchCourse} slice="courseware">
<CourseExit />
</TabContainer>
</PageRoute>
<PageRoute
path={[
'/course/:courseId/:sequenceId/:unitId',
'/course/:courseId/:sequenceId',
'/course/:courseId',
]}
component={CoursewareContainer}
/>
</Switch>
<Footer />
</UserMessagesProvider>
</AppProvider>,
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) => {
@@ -96,7 +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,
NEXBLOCK_API_PATH: process.env.NEXBLOCK_API_PATH || 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');
},
},

View File

@@ -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 {

View File

@@ -1,36 +0,0 @@
import React, { useState, useEffect } from 'react';
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
import Plugin, { COMPONENT } from '../plugin-test/Plugin';
const FALLBACK_URL = 'http://localhost:7331/remoteEntry.js';
const FALLBACK_VIEW = 'PluginOne';
// eslint-disable-next-line react/prefer-stateless-function
export default function NexBlockContainer() {
// eslint-disable-next-line react/prop-types
const query = new URLSearchParams(window.location.search);
const plugin = {
url: query.get('url') ?? FALLBACK_URL,
scope: 'plugin',
module: `./${query.get('view') ?? FALLBACK_VIEW}`,
type: COMPONENT,
};
const [instanceData, setInstanceData] = useState({ title: 'Loading...', body: '' });
const usageId = query.get('usage_id');
const DATA_URL = `http://localhost:18000/api/nexblocks/v0/instance-data/${usageId}`;
const httpClient = getAuthenticatedHttpClient();
useEffect(() => {
httpClient.get(DATA_URL, { params: {} }).then(({ data }) => {
setInstanceData(data);
});
}, []);
return (
<div style={{ marginTop: '2em' }}>
<Plugin plugin={plugin} instanceData={instanceData} />
</div>
);
}

View File

@@ -1,25 +0,0 @@
import { getConfig } from '@edx/frontend-platform';
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
import { NexDataProvider } from '../nex';
export default class NexLmsDataProvider extends NexDataProvider {
constructor(instanceKey, learningContextKey) {
super(instanceKey, learningContextKey);
this.client = getAuthenticatedHttpClient();
const nexApiRoot = getConfig().NEX_LMS_API_ROOT;
this.instanceDataRoot = `${nexApiRoot}/instances/${instanceKey}`;
this.learnerDataRoot = `${nexApiRoot}/instances/${instanceKey}/contexts/${learningContextKey}`;
}
async fetchInstanceData(dataKey) {
return this.client.get(`${this.instanceDataRoot}/data/${dataKey || ''}`);
}
async fetchLearnerData(dataKey) {
return this.client.get(`${this.learnerDataRoot}/data/${dataKey}`);
}
async emitLearnerEvent(eventData) {
return this.client.post(`${this.learnerDataRoot}/events`, eventData);
}
}

View File

@@ -1,9 +0,0 @@
import PropTypes from 'prop-types';
export default function FallbackAuthoringUi({ instanceDataSchema }) { // eslint-disable-line no-unused-vars
return 'Fallback Authoring UI not yet implemented';
}
FallbackAuthoringUi.propTypes = {
instanceDataSchema: PropTypes.object, // eslint-disable-line react/forbid-prop-types
};

View File

@@ -1,22 +0,0 @@
import PropTypes from 'prop-types';
import NexCore from './NexCore';
import FallbackAuthoringUi from './FallbackAuthoringUi';
export default function NexBlock({
core, learnerUi, instructorUi, authorUi, instanceDataSchema,
}) {
const injectCoreIntoProps = ui => (props => ui({ core, ...props }));
this.learnerUi = injectCoreIntoProps(learnerUi);
this.instructorUi = instructorUi ? injectCoreIntoProps(instructorUi) : this.learnerUi;
this.authorUi = authorUi || FallbackAuthoringUi(instanceDataSchema || null);
}
NexBlock.propTypes = {
core: PropTypes.instanceOf(NexCore).isRequired,
learnerUi: PropTypes.elementType.isRequired,
instructorUi: PropTypes.elementType,
authorUi: PropTypes.elementType,
instanceDataSchema: PropTypes.object, // eslint-disable-line react/forbid-prop-types
};

View File

@@ -1,9 +0,0 @@
export default class NexCore {
constructor(dataProvider) {
this.dataProvider = dataProvider;
}
async query(queryData) { // eslint-disable-line no-unused-vars
throw new Error('NexCore.query must be implemented.');
}
}

View File

@@ -1,18 +0,0 @@
export default class NexDataProvider {
constructor(instanceKey, learningContextKey) {
this.instanceKey = instanceKey;
this.learningContextKey = learningContextKey;
}
async fetchInstanceData(dataKey) { // eslint-disable-line no-unused-vars
throw new Error('NexDataProvider.fetchInstanceData must be implemented.');
}
async fetchLearnerData(dataKey) { // eslint-disable-line no-unused-vars
throw new Error('NexDataProvider.fetchLearnerData must be implemented.');
}
async emitLearnerEvent(eventData) { // eslint-disable-line no-unused-vars
throw new Error('NexDataProvider.emitLearnerEvent must be implemented.');
}
}

View File

@@ -1,3 +0,0 @@
export NexBlock from './NexBlock';
export NexCore from './NexCore';
export NexDataProvider from './NexDataProvider';

View File

@@ -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, ...props }) {
function PluginComponent({ plugin, intl, ...props }) {
const url = plugin ? plugin.url : null;
const { ready, failed } = useDynamicScript(url);
@@ -64,7 +68,7 @@ function Plugin({ plugin, intl, ...props }) {
return null;
}
const PluginComponent = React.lazy(
const Component = React.lazy(
loadScriptComponent(plugin.scope, plugin.module),
);
@@ -76,12 +80,14 @@ function Plugin({ plugin, intl, ...props }) {
/>
)}
>
<PluginComponent {...props} {...plugin.props} />
<PluginErrorBoundary>
<Component {...props} {...plugin.props} />
</PluginErrorBoundary>
</Suspense>
);
}
Plugin.propTypes = {
PluginComponent.propTypes = {
plugin: PropTypes.shape({
scope: PropTypes.string.isRequired,
module: PropTypes.string.isRequired,
@@ -92,8 +98,8 @@ Plugin.propTypes = {
intl: intlShape.isRequired,
};
Plugin.defaultProps = {
PluginComponent.defaultProps = {
plugin: null,
};
export default injectIntl(Plugin);
export default injectIntl(PluginComponent);

View File

@@ -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 <div>Plugin failed to load.</div>;
}
return this.props.children;
}
}
PluginErrorBoundary.propTypes = {
children: PropTypes.node,
};
PluginErrorBoundary.defaultProps = {
children: null,
};

View File

@@ -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 (
<div>
<h1>Dynamic Plugin Host</h1>
@@ -34,10 +13,15 @@ function PluginTestPage() {
<strong>remotes</strong> and <strong>exposes</strong>. It will no load
components that have been loaded already.
</p>
<Button className="mr-3" onClick={setPluginOne}>Load Plugin One</Button>
<Button onClick={setPluginTwo}>Load Plugin Two</Button>
{/* <Button className="mr-3" onClick={setPluginOne}>Load Plugin One</Button>
<Button onClick={setPluginTwo}>Load Plugin Two</Button> */}
<div style={{ marginTop: '2em' }}>
<Plugin plugin={plugin} />
{getConfig().plugins.slots.testPage.map((plugin) => (
<PluginComponent
key={`plugin-${plugin.url}-${plugin.module}`}
plugin={plugin}
/>
))}
</div>
</div>
);

View File

@@ -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({
<Helmet>
<title>{`${activeTab.title} | ${title} | ${getConfig().SITE_NAME}`}</title>
</Helmet>
{getConfig().plugins.slots.header.map((plugin) => (
<PluginComponent
key={`plugin-${plugin.url}-${plugin.module}`}
plugin={plugin}
/>
))}
<Header
courseOrg={org}
courseNumber={number}
@@ -49,6 +56,7 @@ function LoadedTabPage({
unitId={unitId}
/>
)}
<main id="main-content" className="d-flex flex-column flex-grow-1">
<AlertList
topic="outline"

View File

@@ -22,10 +22,6 @@ module.exports = createConfig('webpack-dev', {
singleton: true,
eager: true,
},
'@edx/frontend-platform/auth': {
singleton: true,
eager: true,
},
},
}),
],