SubSection nav next/previous buttons work again.

Still working on the unit button rendering.
This commit is contained in:
David Joy
2020-01-10 11:30:10 -05:00
parent 07776c2173
commit 1f79bead57
6 changed files with 450 additions and 74 deletions

View File

@@ -1,4 +1,4 @@
import React, { useState, useEffect, useContext } from 'react';
import React from 'react';
import PropTypes from 'prop-types';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
@@ -6,7 +6,7 @@ import PageLoading from './PageLoading';
import messages from './messages';
import CourseBreadcrumbs from './CourseBreadcrumbs';
// import SubSection from './SubSection';
import SubSection from './SubSection';
import { useCourseStructure } from './hooks';
import CourseStructureContext from './CourseStructureContext';
@@ -37,7 +37,7 @@ function LearningSequencePage({ match, intl }) {
/>}
{loaded && <CourseBreadcrumbs />}
{/* <SubSection /> */}
<SubSection />
</CourseStructureContext.Provider>
</div>
</main>

View File

@@ -0,0 +1,239 @@
import React, { Component, useContext, useEffect, useState } from 'react';
import PropTypes from 'prop-types';
import { getConfig } from '@edx/frontend-platform';
import SubSectionNavigation from './SubSectionNavigation';
import { getSubSectionMetadata } from './api';
import CourseStructureContext from './CourseStructureContext';
import Unit from './Unit';
function useSubSectionMetadata(courseId, subSectionId) {
const [metadata, setMetadata] = useState(null);
const [loaded, setLoaded] = useState(false);
useEffect(() => {
setLoaded(false);
getSubSectionMetadata(courseId, subSectionId).then((data) => {
setMetadata(data);
setLoaded(true);
});
}, [courseId, subSectionId]);
return {
metadata,
loaded,
};
}
function useExamRedirect(metadata, blocks) {
useEffect(() => {
if (metadata !== null && blocks !== null) {
if (metadata.isTimeLimited) {
global.location.href = blocks[metadata.itemId].lmsWebUrl;
}
}
}, [metadata, blocks]);
}
/*
const calculateUnitId = (metadata, options) => {
const { first, last, preferredUnitId } = options;
let position = metadata.position - 1; // metadata's position is 1's indexed
position = first ? 0 : position;
position = last ? metadata.unitIds.length - 1 : position;
position = preferredUnitId ? metadata.unitIds.indexOf(preferredUnitId) : position;
const unitId = metadata.items[position].id;
return unitId;
}
handleUnitChange = (unitId) => {
this.setState({
unitId,
});
}
*/
export default function SubSection() {
const {
courseId,
subSectionId,
unitId,
blocks,
} = useContext(CourseStructureContext);
const { metadata } = useSubSectionMetadata(courseId, subSectionId);
useExamRedirect(metadata, blocks);
if (blocks === null || metadata === null) {
return null;
}
const unit = blocks[unitId];
// units={this.state.units}
// unitIds={this.state.subSectionMetadata.unitIds}
// activeUnitId={this.state.unitId}
// unitClickHandler={this.handleUnitChange}
// nextClickHandler={this.handleNextClick}
// previousClickHandler={this.handlePreviousClick}
return (
<section>
<SubSectionNavigation />
<Unit id={unitId} unit={unit} />
</section>
);
}
/*
<h1>{course.displayName}</h1>
<Breadcrumbs
links={[
{ label: course.displayName, url: global.location.href },
{ label: chapter.displayName, url: global.location.href },
{ label: subSection.displayName, url: global.location.href },
]}
activeLabel={currentUnit.pageTitle}
/>
<SubSectionNavigation
units={this.state.units}
unitIds={this.state.subSectionMetadata.unitIds}
activeUnitId={this.state.unitId}
unitClickHandler={this.handleUnitChange}
nextClickHandler={this.handleNextClick}
previousClickHandler={this.handlePreviousClick}
/>
<iframe
title="yus"
ref={this.iframeRef}
src={iframeUrl}
/>
*/
// constructor(props, context) {
// super(props, context);
// // this.state = {
// // loading: true,
// // blocks: {},
// // units: {},
// // subSectionMetadata: null,
// // subSectionId: null,
// // subSectionIds: [],
// // unitId: null,
// // courseBlockId: null,
// // };
// // this.iframeRef = React.createRef();
// }
// componentDidMount() {
// loadCourseSequence(this.props.match.params.courseId, this.props.match.params.subSectionId, this.props.match.params.unitId, this.context.authenticatedUser.username)
// .then(({
// blocks, courseBlockId, subSectionIds, subSectionMetadata, units, unitId,
// }) => {
// console.log(subSectionMetadata);
// console.log(blocks[subSectionMetadata.itemId].lmsWebUrl);
// // If the sub section is time limited, that means it is some sort of special exam.
// const specialExam = subSectionMetadata.isTimeLimited;
// if (specialExam) {
// global.location.href = blocks[subSectionMetadata.itemId].lmsWebUrl;
// return; // We get out of here to abort loading.
// }
// this.setState({
// loading: false,
// blocks,
// units,
// subSectionMetadata,
// subSectionId: subSectionMetadata.itemId,
// subSectionIds,
// unitId,
// // eslint-disable-next-line react/no-unused-state
// courseBlockId, // TODO: Currently unused, but may be necessary.
// });
// });
// }
// componentDidUpdate(prevProps, prevState) {
// if (
// this.props.match.params.courseId !== prevProps.match.params.courseId ||
// this.state.subSectionId !== prevState.subSectionId ||
// this.state.unitId !== prevState.unitId
// ) {
// history.push(`/course/${this.props.match.params.courseId}/${this.state.subSectionId}/${this.state.unitId}`);
// }
// }
// 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, { last: true }).then(({ subSectionMetadata, units, unitId }) => {
// const specialExam = subSectionMetadata.isTimeLimited;
// if (specialExam) {
// global.location.href = this.state.blocks[subSectionMetadata.itemId].lmsWebUrl;
// return; // We get out of here to abort loading.
// }
// 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, { first: true })
// .then(({ subSectionMetadata, units, unitId }) => {
// const specialExam = subSectionMetadata.isTimeLimited;
// if (specialExam) {
// global.location.href = this.state.blocks[subSectionMetadata.itemId].lmsWebUrl;
// return; // We get out of here to abort loading.
// }
// this.setState({
// subSectionId: subSectionMetadata.itemId,
// subSectionMetadata,
// units,
// unitId,
// });
// });
// } else {
// console.log('we are at the end!');
// }
// }
// }

View File

@@ -1,79 +1,105 @@
import React, { Component } from 'react';
import React, { Component, useCallback, useContext } from 'react';
import PropTypes from 'prop-types';
import { history } from '@edx/frontend-platform';
import { Button } from '@edx/paragon';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faFilm, faBook, faPencilAlt, faTasks } from '@fortawesome/free-solid-svg-icons';
export default class SubSectionNavigation extends Component {
renderUnitIcon(type) {
let icon = null;
switch (type) {
case 'video':
icon = faFilm;
break;
case 'other':
icon = faBook;
break;
case 'vertical':
icon = faTasks;
break;
case 'problem':
icon = faPencilAlt;
break;
default:
icon = faBook;
}
return <FontAwesomeIcon icon={icon} />;
import { useCurrentSubSection, useCurrentUnit, usePreviousUnit, useNextUnit, useCurrentCourse, useCurrentSubSectionUnits } from './hooks';
import CourseStructureContext from './CourseStructureContext';
function UnitIcon({ type }) {
let icon = null;
switch (type) {
case 'video':
icon = faFilm;
break;
case 'other':
icon = faBook;
break;
case 'vertical':
icon = faTasks;
break;
case 'problem':
icon = faPencilAlt;
break;
default:
icon = faBook;
}
renderUnits() {
return this.props.unitIds.map((id) => {
const { type } = this.props.units[id];
const disabled = this.props.activeUnitId === id;
return (
<Button
key={id}
className="btn-outline-secondary unit-button"
onClick={() => this.props.unitClickHandler(id)}
disabled={disabled}
>
{this.renderUnitIcon(type)}
</Button>
);
});
}
render() {
return (
<nav>
<Button
key="previous"
className="btn-outline-primary"
onClick={this.props.previousClickHandler}
>
Previous
</Button>
{this.renderUnits()}
<Button
key="next"
className="btn-outline-primary"
onClick={this.props.nextClickHandler}
>
Next
</Button>
</nav>
);
}
return (
<FontAwesomeIcon icon={icon} />
);
}
export default function SubSectionNavigation() {
const { courseId } = useContext(CourseStructureContext);
const subSection = useCurrentSubSection();
const previousUnit = usePreviousUnit();
const nextUnit = useNextUnit();
const handlePreviousClick = useCallback(() => {
if (previousUnit) {
history.push(`/course/${courseId}/${subSection.id}/${previousUnit.id}`);
}
});
const handleNextClick = useCallback(() => {
if (nextUnit) {
history.push(`/course/${courseId}/${subSection.id}/${nextUnit.id}`);
}
});
return (
<nav>
<Button
key="previous"
className="btn-outline-primary"
onClick={handlePreviousClick}
>
Previous
</Button>
{/* {this.renderUnits()} */}
<Button
key="next"
className="btn-outline-primary"
onClick={handleNextClick}
>
Next
</Button>
</nav>
);
}
function UnitNavigation() {
const units = useCurrentSubSectionUnits();
}
function renderUnits() {
return this.props.unitIds.map((id) => {
const { type } = this.props.units[id];
const disabled = this.props.activeUnitId === id;
return (
<Button
key={id}
className="btn-outline-secondary unit-button"
onClick={() => this.props.unitClickHandler(id)}
disabled={disabled}
>
{this.renderUnitIcon(type)}
</Button>
);
});
}
SubSectionNavigation.propTypes = {
unitIds: PropTypes.arrayOf(PropTypes.string).isRequired,
units: PropTypes.objectOf(PropTypes.shape({
pageTitle: PropTypes.string.isRequired,
type: PropTypes.oneOf(['video', 'other', 'vertical', 'problem']).isRequired,
})).isRequired,
activeUnitId: PropTypes.string.isRequired,
unitClickHandler: PropTypes.func.isRequired,
nextClickHandler: PropTypes.func.isRequired,
previousClickHandler: PropTypes.func.isRequired,
// unitIds: PropTypes.arrayOf(PropTypes.string).isRequired,
// units: PropTypes.objectOf(PropTypes.shape({
// pageTitle: PropTypes.string.isRequired,
// type: PropTypes.oneOf(['video', 'other', 'vertical', 'problem']).isRequired,
// })).isRequired,
// activeUnitId: PropTypes.string.isRequired,
// unitClickHandler: PropTypes.func.isRequired,
// nextClickHandler: PropTypes.func.isRequired,
// previousClickHandler: PropTypes.func.isRequired,
};

View File

@@ -0,0 +1,23 @@
import React, { useRef } from 'react';
import PropTypes from 'prop-types';
import { getConfig } from '@edx/frontend-platform';
export default function Unit({ id, unit }) {
const iframeRef = useRef(null);
const iframeUrl = `${getConfig().LMS_BASE_URL}/xblock/${id}`;
const { displayName } = unit;
return (
<iframe
title={displayName}
ref={iframeRef}
src={iframeUrl}
/>
);
}
Unit.propTypes = {
id: PropTypes.string.isRequired,
unit: PropTypes.shape({
displayName: PropTypes.string.isRequired,
}).isRequired,
};

View File

@@ -60,7 +60,7 @@ export async function loadSubSectionMetadata(courseId, subSectionId, {
};
}
async function getSubSectionMetadata(courseId, subSectionId) {
export async function getSubSectionMetadata(courseId, subSectionId) {
const { data } = await getAuthenticatedHttpClient()
.get(`${getConfig().LMS_BASE_URL}/courses/${courseId}/xblock/${subSectionId}/handler/xmodule_handler/metadata`, {});
@@ -126,6 +126,20 @@ export function createSubSectionIdList(blocks, entryPointId, subSections = []) {
return subSections;
}
export function createUnitIdList(blocks, entryPointId, units = []) {
const block = blocks[entryPointId];
if (block.type === 'vertical') {
units.push(block.id);
}
if (Array.isArray(block.children)) {
for (let i = 0; i < block.children.length; i++) {
const childId = block.children[i];
createUnitIdList(blocks, childId, units);
}
}
return units;
}
export function findBlockAncestry(blocks, blockId, descendents = []) {
const block = blocks[blockId];
descendents.unshift(block);

View File

@@ -2,7 +2,7 @@ import { useContext, useMemo, useState, useEffect } from 'react';
import { AppContext } from '@edx/frontend-platform/react';
import CourseStructureContext from './CourseStructureContext';
import { findBlockAncestry, getCourseBlocks, createBlocksMap } from './api';
import { findBlockAncestry, getCourseBlocks, createBlocksMap, createSubSectionIdList, createUnitIdList } from './api';
export function useBlockAncestry(blockId) {
const { blocks, loaded } = useContext(CourseStructureContext);
@@ -20,7 +20,7 @@ export function useBlockAncestry(blockId) {
export function useCourseStructure(courseId) {
const { authenticatedUser } = useContext(AppContext);
const [blocks, setBlocks] = useState({});
const [blocks, setBlocks] = useState(null);
const [loaded, setLoaded] = useState(false);
const [courseBlockId, setCourseBlockId] = useState();
@@ -37,3 +37,77 @@ export function useCourseStructure(courseId) {
blocks, loaded, courseBlockId,
};
}
export function useCurrentCourse() {
const { loaded, courseBlockId, blocks } = useContext(CourseStructureContext);
return loaded ? blocks[courseBlockId] : null;
}
export function useCurrentSubSection() {
const { loaded, blocks, subSectionId } = useContext(CourseStructureContext);
return loaded ? blocks[subSectionId] : null;
}
export function useCurrentSection() {
const { loaded, blocks } = useContext(CourseStructureContext);
const subSection = useCurrentSubSection();
return loaded ? blocks[subSection.parentId] : null;
}
export function useCurrentUnit() {
const { loaded, blocks, unitId } = useContext(CourseStructureContext);
return loaded ? blocks[unitId] : null;
}
export function useUnitIds() {
const { loaded, blocks, courseBlockId } = useContext(CourseStructureContext);
return useMemo(
() => (loaded ? createUnitIdList(blocks, courseBlockId) : []),
[loaded, blocks, courseBlockId],
);
}
export function usePreviousUnit() {
const { loaded, blocks, unitId } = useContext(CourseStructureContext);
const unitIds = useUnitIds();
const currentUnitIndex = unitIds.indexOf(unitId);
if (currentUnitIndex === 0) {
return null;
}
return loaded ? blocks[unitIds[currentUnitIndex - 1]] : null;
}
export function useNextUnit() {
const { loaded, blocks, unitId } = useContext(CourseStructureContext);
const unitIds = useUnitIds();
const currentUnitIndex = unitIds.indexOf(unitId);
if (currentUnitIndex === unitIds.length - 1) {
return null;
}
return loaded ? blocks[unitIds[currentUnitIndex + 1]] : null;
}
export function useCurrentSubSectionUnits() {
const { blocks } = useContext(CourseStructureContext);
const subSection = useCurrentSubSection();
return subSection.children.map(id => blocks[id]);
}
export function useSubSectionIdList() {
const { loaded, blocks, courseBlockId } = useContext(CourseStructureContext);
const subSectionIdList = useMemo(
() => (loaded ? createSubSectionIdList(blocks, courseBlockId) : []),
[blocks, courseBlockId],
);
return subSectionIdList;
}