From 446367e56c6aea8df86027a24d1e65050f2474fe Mon Sep 17 00:00:00 2001 From: David Joy Date: Mon, 6 Jan 2020 15:43:13 -0500 Subject: [PATCH] feat: sub section navigation working, allows navigating between all units --- .../LearningSequencePage.jsx | 178 ++++++++++-------- .../SubSectionNavigation.jsx | 65 ++++++- src/learning-sequence/api.js | 128 +++++++++++++ 3 files changed, 281 insertions(+), 90 deletions(-) create mode 100644 src/learning-sequence/api.js diff --git a/src/learning-sequence/LearningSequencePage.jsx b/src/learning-sequence/LearningSequencePage.jsx index 0aa612e4..8bf9ac0f 100644 --- a/src/learning-sequence/LearningSequencePage.jsx +++ b/src/learning-sequence/LearningSequencePage.jsx @@ -1,101 +1,108 @@ +/* eslint-disable no-plusplus */ import React, { Component } from 'react'; import PropTypes from 'prop-types'; -import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth'; import { injectIntl, intlShape } from '@edx/frontend-platform/i18n'; -import { getConfig, camelCaseObject } from '@edx/frontend-platform'; +import { getConfig, history } 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'; - -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}`), ''); - - const { data } = await getAuthenticatedHttpClient() - .get(`${getConfig().LMS_BASE_URL}/api/courses/v2/blocks/${queryParams}`, {}); - - return { models: organizeCourseModels(data.blocks), courseBlockId: data.root }; -} - -function organizeCourseModels(blocksMap) { - const models = {}; - - const blocks = Object.values(blocksMap); - for (let i = 0; i < blocks.length; i++) { - const block = blocks[i]; - models[block.id] = camelCaseObject(block); - } - - // 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); -} +import { loadCourseSequence, findBlockAncestry, loadSubSectionMetadata } from './api'; class LearningSequencePage extends Component { constructor(props, context) { super(props, context); this.state = { - models: {}, - courseBlockId: null, loading: true, - currentUnitId: null, + blocks: {}, + units: {}, + subSectionMetadata: null, + subSectionId: null, + subSectionIds: [], + unitId: null, + courseBlockId: 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. + loadCourseSequence(this.props.match.params.courseId, null, this.context.authenticatedUser.username) + .then(({ + blocks, courseBlockId, subSectionIds, subSectionMetadata, units, unitId, + }) => { this.setState({ - models, - courseBlockId, loading: false, - currentUnitId: currentUnit.id, + blocks, + units, + subSectionMetadata, + subSectionId: subSectionMetadata.itemId, + subSectionIds, + unitId, + courseBlockId, // TODO: Currently unused, but may be necessary. }); }); } + handlePreviousClick = () => { + const index = this.state.subSectionMetadata.unitIds.indexOf(this.state.unitId); + if (index > 0) { + this.setState({ + unitId: this.state.subSectionMetadata.unitIds[index - 1], + }); + } else { + const subSectionIndex = this.state.subSectionIds.indexOf(this.state.subSectionId); + if (subSectionIndex > 0) { + const previousSubSectionId = this.state.subSectionIds[subSectionIndex - 1]; + + loadSubSectionMetadata(this.props.match.params.courseId, previousSubSectionId).then(({ subSectionMetadata, units, unitId }) => { + this.setState({ + subSectionId: subSectionMetadata.itemId, + subSectionMetadata, + units, + unitId, + }); + }); + } else { + console.log('we are at the beginning!'); + // TODO: We need to calculate whether we're on the first/last subSection in render so we can + // disable the Next/Previous buttons. That'll involve extracting a bit of logic from this + // function and handleNextClick below and reusing it - memoized, probably - in render(). + } + } + } + + handleNextClick = () => { + const index = this.state.subSectionMetadata.unitIds.indexOf(this.state.unitId); + if (index < this.state.subSectionMetadata.unitIds.length - 1) { + this.setState({ + unitId: this.state.subSectionMetadata.unitIds[index + 1], + }); + } else { + const subSectionIndex = this.state.subSectionIds.indexOf(this.state.subSectionId); + if (subSectionIndex < this.state.subSectionIds.length - 1) { + const nextSubSectionId = this.state.subSectionIds[subSectionIndex + 1]; + + loadSubSectionMetadata(this.props.match.params.courseId, nextSubSectionId).then(({ subSectionMetadata, units, unitId }) => { + this.setState({ + subSectionId: subSectionMetadata.itemId, + subSectionMetadata, + units, + unitId, + }); + }); + } else { + console.log('we are at the end!'); + } + } + } + handleUnitChange = (unitId) => { this.setState({ - currentUnitId: unitId, + unitId, }); } @@ -106,33 +113,40 @@ class LearningSequencePage extends Component { ); } - const currentUnit = this.state.models[this.state.currentUnitId]; + const [course, chapter, subSection] = findBlockAncestry( + this.state.blocks, + this.state.blocks[this.state.subSectionId], + ); - // 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); + const currentUnit = this.state.units[this.state.unitId]; + const iframeUrl = `${getConfig().LMS_BASE_URL}/xblock/${this.state.unitId}`; return (

{course.displayName}

>} /> - +