Compare commits

...

9 Commits

Author SHA1 Message Date
Kyle McCormick
17b027a8db fixes from linting 2021-02-15 22:01:09 -05:00
Kyle McCormick
74e86fd4e8 settling on NexBases 2021-02-15 21:39:25 -05:00
Kyle McCormick
40bcbb60bf ideating on nex base classes 2021-02-15 14:19:09 -05:00
Kyle McCormick
b1b0a00bd1 wip: begin factoring out api dependency 2021-02-12 22:12:12 -05:00
Kyle McCormick
ae105afe4c Merge branch 'hack/nexblock' of github.com:edx/frontend-app-learning into hack/nexblock 2021-02-12 10:14:05 -05:00
Kyle McCormick
cd4f8d3e01 Make it work with backend 2021-02-12 10:13:40 -05:00
Jie Han
a56ba1a3d4 allow plugins to use the auth package 2021-02-11 15:35:34 -05:00
Kyle McCormick
5796b87341 refactor nexblock package 2021-02-11 13:04:28 -05:00
Kyle McCormick
7ecc0e7929 add /nexblock?view=X&url=y page for rendering single nexblock 2021-02-10 14:51:19 -05:00
12 changed files with 168 additions and 33 deletions

1
.env
View File

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

View File

@@ -29,3 +29,5 @@ 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

@@ -27,6 +27,7 @@ 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';
subscribe(APP_READY, () => {
@@ -34,38 +35,41 @@ subscribe(APP_READY, () => {
<AppProvider store={initializeStore()}>
<UserMessagesProvider>
<Switch>
<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}
/>
<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 />
</Switch>
<Footer />
</UserMessagesProvider>
</AppProvider>,
document.getElementById('root'),
@@ -92,6 +96,7 @@ 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,
}, 'LearnerAppConfig');
},
},

View File

@@ -0,0 +1,36 @@
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

@@ -0,0 +1,25 @@
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

@@ -0,0 +1,9 @@
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
};

22
src/nex/NexBlock.js Normal file
View File

@@ -0,0 +1,22 @@
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
};

9
src/nex/NexCore.js Normal file
View File

@@ -0,0 +1,9 @@
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

@@ -0,0 +1,18 @@
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.');
}
}

3
src/nex/index.js Normal file
View File

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

View File

@@ -44,7 +44,7 @@ const useDynamicScript = (url) => {
};
};
function Plugin({ plugin, intl }) {
function Plugin({ plugin, intl, ...props }) {
const url = plugin ? plugin.url : null;
const { ready, failed } = useDynamicScript(url);
@@ -76,7 +76,7 @@ function Plugin({ plugin, intl }) {
/>
)}
>
<PluginComponent />
<PluginComponent {...props} {...plugin.props} />
</Suspense>
);
}
@@ -87,6 +87,7 @@ Plugin.propTypes = {
module: PropTypes.string.isRequired,
url: PropTypes.string.isRequired,
type: PropTypes.oneOf([COMPONENT, SCRIPT, IFRAME]).isRequired,
props: PropTypes.object,
}),
intl: intlShape.isRequired,
};

View File

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