diff --git a/public/index.html b/public/index.html
index 17abf3a6..3f8576ac 100644
--- a/public/index.html
+++ b/public/index.html
@@ -1,7 +1,7 @@
- Application Template | edX
+ Course | edX
diff --git a/src/index.scss b/src/index.scss
index ab4e5a33..4f14c9c3 100755
--- a/src/index.scss
+++ b/src/index.scss
@@ -4,3 +4,42 @@
@import "~@edx/frontend-component-header/dist/index";
@import "~@edx/frontend-component-footer/dist/footer";
+
+#root {
+ display: flex;
+ flex-direction: column;
+ height: 100vh;
+
+ header {
+ flex: 0;
+ }
+
+ main {
+ flex: 1;
+ display: flex;
+ flex-direction: column;
+
+ iframe {
+ flex: 1;
+ margin: 0 -10px;
+ }
+
+ nav {
+ display: flex;
+ flex-direction: row;
+ width: 100%;
+
+ .unit-button {
+ flex: 1;
+ }
+
+ button {
+ margin: 0px 5px;
+ }
+ }
+ }
+
+ footer {
+ flex: 0;
+ }
+}
diff --git a/src/learning-sequence/LearningSequencePage.jsx b/src/learning-sequence/LearningSequencePage.jsx
index 6f8161c4..0aa612e4 100644
--- a/src/learning-sequence/LearningSequencePage.jsx
+++ b/src/learning-sequence/LearningSequencePage.jsx
@@ -1,116 +1,154 @@
-import React, { useState, useEffect, useCallback, useRef } from 'react';
+import React, { Component } from 'react';
import PropTypes from 'prop-types';
-import { camelCaseObject, getConfig } from '@edx/frontend-platform';
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
+import { getConfig, camelCaseObject } from '@edx/frontend-platform';
+import { AppContext } from '@edx/frontend-platform/react';
+import { Breadcrumb } from '@edx/paragon';
import PageLoading from './PageLoading';
-
import messages from './messages';
+import SubSectionNavigation from './SubSectionNavigation';
-function useApi(apiFunction, {
- format = true, keepDataIfFailed = false, loadedIfFailed = false, refreshParams = [],
-}) {
- const [data, setData] = useState(null);
- const [loading, setLoading] = useState(false);
- const [loaded, setLoaded] = useState(false);
- const [failed, setFailed] = useState(false);
- const [error, setError] = useState(null);
+async function getCourseBlocks(courseId, username) {
+ const queryParams = Object.entries({
+ course_id: courseId,
+ username,
+ depth: 3,
+ requested_fields: 'children',
+ }).reduce((acc, [key, value]) => (acc === '' ? `?${key}=${value}` : `${acc}&${key}=${value}`), '');
- useEffect(() => {
- setLoading(true);
- apiFunction().then((response) => {
- const result = format ? camelCaseObject(response.data) : response.data;
- setData(result);
- setLoaded(true);
- setLoading(false);
- setError(null);
- setFailed(false);
- })
- .catch((e) => {
- if (keepDataIfFailed) {
- setData(null);
- }
- setFailed(true);
- setLoading(false);
- if (loadedIfFailed) {
- setLoaded(true);
- }
- setError(e);
- });
- }, refreshParams);
+ const { data } = await getAuthenticatedHttpClient()
+ .get(`${getConfig().LMS_BASE_URL}/api/courses/v2/blocks/${queryParams}`, {});
- return {
- data,
- loading,
- loaded,
- failed,
- error,
- };
+ return { models: organizeCourseModels(data.blocks), courseBlockId: data.root };
}
-function LearningSequencePage(props) {
- const iframeRef = useRef(null);
+function organizeCourseModels(blocksMap) {
+ const models = {};
- const handleResizeIframe = useCallback(() => {
- // TODO: This won't work because of crossdomain issues. Leaving here for reference once we're
- // able to have the iFrame content publish resize events through postMessage
- console.log('**** Resizing iframe...');
- const iframe = iframeRef.current;
- const contentHeight = iframe.contentWindow.document.body.scrollHeight;
- console.log(`**** Height is: ${contentHeight}`);
- iframe.height = contentHeight + 20;
- });
-
- const {
- data,
- loading,
- loaded,
- } = useApi(
- async () => getAuthenticatedHttpClient().get(`${getConfig().LMS_BASE_URL}/api/courses/v1/blocks/?course_id=${props.match.params.courseId}&username=staff&depth=all&block_types_filter=sequential&requested_fields=children`, {}),
- {
- keepDataIfFailed: false,
- refreshParams: [
- props.match.params.courseId,
- props.match.params.blockIndex,
- ],
- },
- );
-
- console.log(data);
-
- if (loading) {
- return (
-
- );
+ const blocks = Object.values(blocksMap);
+ for (let i = 0; i < blocks.length; i++) {
+ const block = blocks[i];
+ models[block.id] = camelCaseObject(block);
}
- return (
-
-
-
Learning Sequence Page
- {loaded && data.blocks ? (
-
- ) : null}
-
-
- );
+ // NOTE: If a child is listed as a child of multiple models, the last one in wins. This does NOT
+ // support multiple parents.
+ const modelValues = Object.values(models);
+ for (let i = 0; i < modelValues.length; i++) {
+ const model = modelValues[i];
+
+ if (Array.isArray(model.children)) {
+ for (let j = 0; j < model.children.length; j++) {
+ const child = models[model.children[j]];
+ child.parentId = model.id;
+ }
+ }
+ }
+
+ return models;
}
+function findFirstLeafChild(models, blockId) {
+ const block = models[blockId];
+ if (Array.isArray(block.children) && block.children.length > 0) {
+ return findFirstLeafChild(models, block.children[0]);
+ }
+ return block;
+}
+
+function findBlockAncestry(models, block, descendents = []) {
+ descendents.unshift(block);
+ if (block.parentId === undefined) {
+ return descendents;
+ }
+ return findBlockAncestry(models, models[block.parentId], descendents);
+}
+
+class LearningSequencePage extends Component {
+ constructor(props, context) {
+ super(props, context);
+
+ this.state = {
+ models: {},
+ courseBlockId: null,
+ loading: true,
+ currentUnitId: null,
+ };
+
+ this.iframeRef = React.createRef();
+ }
+
+ componentDidMount() {
+ getCourseBlocks(this.props.match.params.courseId, this.context.authenticatedUser.username)
+ .then(({ models, courseBlockId }) => {
+ const currentUnit = findFirstLeafChild(models, courseBlockId); // Temporary until we know where the user is in the course.
+ this.setState({
+ models,
+ courseBlockId,
+ loading: false,
+ currentUnitId: currentUnit.id,
+ });
+ });
+ }
+
+ handleUnitChange = (unitId) => {
+ this.setState({
+ currentUnitId: unitId,
+ });
+ }
+
+ render() {
+ if (this.state.loading) {
+ return (
+
+ );
+ }
+
+ const currentUnit = this.state.models[this.state.currentUnitId];
+
+ // TODO: All of this should be put in state or memoized.
+ const course = this.state.models[this.state.courseBlockId];
+ const chapter = this.state.models[course.children[0].id];
+ const subSection = this.state.models[currentUnit.parentId];
+ const ancestry = findBlockAncestry(this.state.models, currentUnit);
+ const breadcrumbLinks = ancestry.slice(0, ancestry.length - 1).map(ancestor => ({ label: ancestor.displayName, url: global.location.href }));
+
+
+ console.log(course, chapter, currentUnit, ancestry);
+
+ return (
+
+
+
{course.displayName}
+ >}
+ />
+
+
+
+
+ );
+ }
+}
+
+LearningSequencePage.contextType = AppContext;
+
+export default injectIntl(LearningSequencePage);
+
LearningSequencePage.propTypes = {
match: PropTypes.shape({
params: PropTypes.shape({
courseId: PropTypes.string.isRequired,
- blockIndex: PropTypes.number.isRequired,
+ blockIndex: PropTypes.string.isRequired,
}).isRequired,
}).isRequired,
intl: intlShape.isRequired,
};
-
-export default injectIntl(LearningSequencePage);
diff --git a/src/learning-sequence/SubSectionNavigation.jsx b/src/learning-sequence/SubSectionNavigation.jsx
new file mode 100644
index 00000000..9ab56055
--- /dev/null
+++ b/src/learning-sequence/SubSectionNavigation.jsx
@@ -0,0 +1,30 @@
+import React, { Component } from 'react';
+import { Button } from '@edx/paragon';
+
+export default class SubSectionNavigation extends Component {
+ renderUnits() {
+ return this.props.subSection.children.map((unitId) => {
+ const unit = this.props.models[unitId];
+ return (
+
+ );
+ });
+ }
+
+ render() {
+ return (
+
+ );
+ }
+}