Cleaning up old implementation code.

This commit is contained in:
David Joy
2020-01-15 11:08:01 -05:00
parent 41ab9fc68e
commit 9d9b65ceb9
16 changed files with 118 additions and 624 deletions

View File

@@ -1,27 +1,67 @@
import React, { useEffect } from 'react';
import React, { useEffect, useContext, useState } from 'react';
import PropTypes from 'prop-types';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { history, getConfig } from '@edx/frontend-platform';
import { AppContext } from '@edx/frontend-platform/react';
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
import { useLoadCourseStructure } from './data/hooks';
import messages from './messages';
import PageLoading from './PageLoading';
import Course from './course/Course';
import { history } from '@edx/frontend-platform';
import { createBlocksMap } from './utils';
export async function getCourseBlocks(courseId, username) {
const url = new URL(`${getConfig().LMS_BASE_URL}/api/courses/v2/blocks/`);
url.searchParams.append('course_id', decodeURIComponent(courseId));
url.searchParams.append('username', username);
url.searchParams.append('depth', 3);
url.searchParams.append('requested_fields', 'children,show_gated_sections');
const { data } = await getAuthenticatedHttpClient().get(url.href, {});
return data;
}
export async function getCourse(courseId) {
const url = `${getConfig().LMS_BASE_URL}/api/courses/v2/courses/${courseId}`;
const { data } = await getAuthenticatedHttpClient().get(url);
return data;
}
function useLoadCourse(courseUsageKey) {
const { authenticatedUser } = useContext(AppContext);
const [models, setModels] = useState(null);
const [courseId, setCourseId] = useState();
useEffect(() => {
getCourseBlocks(courseUsageKey, authenticatedUser.username).then((blocksData) => {
setModels(createBlocksMap(blocksData.blocks));
setCourseId(blocksData.root);
});
}, [courseUsageKey]);
return {
models, courseId,
};
}
function CourseContainer({
courseUsageKey, sequenceId, unitId, intl,
}) {
const { blocks, loaded, courseId } = useLoadCourseStructure(courseUsageKey);
const { models, courseId } = useLoadCourse(courseUsageKey);
useEffect(() => {
if (!sequenceId) {
// TODO: This will not work right now.
const { activeSequenceId } = blocks[courseId];
const { activeSequenceId } = models[courseId];
history.push(`/course/${courseUsageKey}/${activeSequenceId}`);
}
}, [courseUsageKey, courseId, sequenceId]);
if (!loaded || !sequenceId) {
if (!courseId || !sequenceId) {
return (
<PageLoading
srMessage={intl.formatMessage(messages['learn.loading.learning.sequence'])}
@@ -35,7 +75,7 @@ function CourseContainer({
courseId={courseId}
sequenceId={sequenceId}
unitId={unitId}
models={blocks}
models={models}
/>
);
}
@@ -47,4 +87,9 @@ CourseContainer.propTypes = {
intl: intlShape.isRequired,
};
CourseContainer.defaultProps = {
sequenceId: null,
unitId: null,
};
export default injectIntl(CourseContainer);

View File

@@ -1,5 +0,0 @@
import React from 'react';
const CourseStructureContext = React.createContext({});
export default CourseStructureContext;

View File

@@ -1,46 +1,24 @@
import React, { useEffect } from 'react';
import React from 'react';
import PropTypes from 'prop-types';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import CourseContainer from './CourseContainer';
function LearningSequencePage({ match, intl }) {
export default function LearningSequencePage({ match }) {
const {
courseUsageKey,
sequenceId,
unitId,
} = match.params;
// const { blocks, loaded, courseId } = useLoadCourseStructure(courseId);
// useMissingSequenceRedirect(loaded, blocks, courseId, courseId, sequenceId);
return (
<CourseContainer courseUsageKey={courseUsageKey} sequenceId={sequenceId} unitId={unitId} />
// <main className="container-fluid d-flex flex-column flex-grow-1">
// <CourseStructureContext.Provider value={{
// courseId,
// courseId,
// sequenceId,
// unitId,
// blocks,
// loaded,
// }}
// >
// {!loaded && <PageLoading
// srMessage={intl.formatMessage(messages['learn.loading.learning.sequence'])}
// />}
// {loaded && unitId && <CourseBreadcrumbs />}
// {sequenceId && <Sequence />}
// </CourseStructureContext.Provider>
// </main>
<CourseContainer
courseUsageKey={courseUsageKey}
sequenceId={sequenceId}
unitId={unitId}
/>
);
}
export default injectIntl(LearningSequencePage);
LearningSequencePage.propTypes = {
match: PropTypes.shape({
params: PropTypes.shape({
@@ -49,5 +27,4 @@ LearningSequencePage.propTypes = {
unitId: PropTypes.string,
}).isRequired,
}).isRequired,
intl: intlShape.isRequired,
};

View File

@@ -4,7 +4,7 @@ import { getConfig, history } from '@edx/frontend-platform';
import CourseBreadcrumbs from './CourseBreadcrumbs';
import SequenceContainer from './SequenceContainer';
import { createSequenceIdList } from '../data/utils';
import { createSequenceIdList } from '../utils';
export default function Course({
courseUsageKey, courseId, sequenceId, unitId, models,

View File

@@ -1,49 +1,54 @@
import React, { useEffect } from 'react';
/* eslint-disable no-plusplus */
import React, { useEffect, useState } from 'react';
import PropTypes from 'prop-types';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { history } from '@edx/frontend-platform';
import { history, camelCaseObject, getConfig } from '@edx/frontend-platform';
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
import { useLoadSequenceMetadata } from './hooks';
import messages from '../messages';
import PageLoading from '../PageLoading';
import Sequence from '../sequence/Sequence';
/*
elementId: "edx_introduction"
bannerText: null
displayName: "Demo Course Overview"
items: Array(1)
0:
path: "Introduction > Demo Course Overview > Introduction: Video and Sequences"
href: ""
type: "video"
content: ""
graded: false
pageTitle: "Introduction: Video and Sequences"
bookmarked: false
id: "block-v1:edX+DemoX+Demo_Course+type@vertical+block@vertical_0270f6de40fc"
complete: null
__proto__: Object
length: 1
__proto__: Array(0)
savePosition: true
isTimeLimited: false
gatedContent: {gatedSectionName: "Demo Course Overview", prereqUrl: null, prereqSectionName: null, gated: false, prereqId: null}
excludeUnits: true
tag: "sequential"
position: 1
showCompletion: true
itemId: "block-v1:edX+DemoX+Demo_Course+type@sequential+block@edx_introduction"
ajaxUrl: "/courses/course-v1:edX+DemoX+Demo_Course/xblock/block-v1:edX+DemoX+Demo_Course+type@sequential+block@edx_introduction/handler/xmodule_handler"
nextUrl: null
prevUrl: null
*/
export async function getSequenceMetadata(courseUsageKey, sequenceId) {
const { data } = await getAuthenticatedHttpClient()
.get(`${getConfig().LMS_BASE_URL}/courses/${courseUsageKey}/xblock/${sequenceId}/handler/xmodule_handler/metadata`, {});
return data;
}
function useLoadSequence(courseUsageKey, sequenceId) {
const [metadata, setMetadata] = useState(null);
const [units, setUnits] = useState(null);
const [loaded, setLoaded] = useState(false);
useEffect(() => {
setLoaded(false);
setMetadata(null);
getSequenceMetadata(courseUsageKey, sequenceId).then((data) => {
const unitsMap = {};
for (let i = 0; i < data.items.length; i++) {
const item = data.items[i];
unitsMap[item.id] = camelCaseObject(item);
}
setMetadata(camelCaseObject(data));
setUnits(unitsMap);
setLoaded(true);
});
}, [courseUsageKey, sequenceId]);
return {
metadata,
units,
loaded,
};
}
function SequenceContainer({
courseUsageKey, courseId, sequenceId, unitId, models, intl, onNext, onPrevious,
}) {
const { metadata, loaded, units } = useLoadSequenceMetadata(courseUsageKey, sequenceId);
console.log(units);
const { metadata, loaded, units } = useLoadSequence(courseUsageKey, sequenceId);
useEffect(() => {
if (loaded && !unitId) {
const position = metadata.position - 1;
@@ -52,6 +57,7 @@ function SequenceContainer({
}
}, [loaded, metadata, unitId]);
// Exam redirect
useEffect(() => {
if (metadata && models) {
if (metadata.isTimeLimited) {
@@ -60,7 +66,6 @@ function SequenceContainer({
}
}, [metadata, models]);
console.log(metadata);
if (!loaded || !unitId || (metadata && metadata.isTimeLimited)) {
return (
<PageLoading
@@ -99,13 +104,19 @@ function SequenceContainer({
bannerText={bannerText}
onNext={onNext}
onPrevious={onPrevious}
onNavigateUnit={() => console.log('hah2')}
prerequisite={prerequisite}
/>
);
}
SequenceContainer.propTypes = {
onNext: PropTypes.func.isRequired,
onPrevious: PropTypes.func.isRequired,
courseUsageKey: PropTypes.string.isRequired,
models: PropTypes.objectOf(PropTypes.shape({
id: PropTypes.string.isRequired,
lmsWebUrl: PropTypes.string.isRequired,
})).isRequired,
courseId: PropTypes.string.isRequired,
sequenceId: PropTypes.string.isRequired,
unitId: PropTypes.string.isRequired,

View File

@@ -1,33 +0,0 @@
import { getConfig } from '@edx/frontend-platform';
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
/* eslint-disable import/prefer-default-export */
const getSequenceXModuleHandlerUrl = (courseUsageKey, sequenceId) =>
`${getConfig().LMS_BASE_URL}/courses/${courseUsageKey}/xblock/${sequenceId}/handler/xmodule_handler`;
export async function getSequenceMetadata(courseUsageKey, sequenceId) {
const { data } = await getAuthenticatedHttpClient()
.get(`${getSequenceXModuleHandlerUrl(courseUsageKey, sequenceId)}/metadata`, {});
return data;
}
export async function saveSequencePosition(courseUsageKey, sequenceId, position) {
// Post data sent to this endpoint must be url encoded
// TODO: Remove the need for this to be the case.
// TODO: Ensure this usage of URLSearchParams is working in Internet Explorer
const urlEncoded = new URLSearchParams();
urlEncoded.append('position', position);
const requestConfig = {
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
};
const { data } = await getAuthenticatedHttpClient().post(
`${getSequenceXModuleHandlerUrl(courseUsageKey, sequenceId)}/goto_position`,
urlEncoded.toString(),
requestConfig,
);
return data;
}

View File

@@ -1,85 +0,0 @@
/* eslint-disable no-plusplus */
import { useState, useEffect, useContext } from 'react';
import { camelCaseObject, history } from '@edx/frontend-platform';
import { getSequenceMetadata, saveSequencePosition } from './api';
import CourseStructureContext from '../CourseStructureContext';
export function useLoadSequenceMetadata(courseUsageKey, sequenceId) {
const [metadata, setMetadata] = useState(null);
const [units, setUnits] = useState(null);
const [loaded, setLoaded] = useState(false);
useEffect(() => {
setLoaded(false);
setMetadata(null);
getSequenceMetadata(courseUsageKey, sequenceId).then((data) => {
const unitsMap = {};
for (let i = 0; i < data.items.length; i++) {
const item = data.items[i];
unitsMap[item.id] = camelCaseObject(item);
}
setMetadata(camelCaseObject(data));
setUnits(unitsMap);
setLoaded(true);
});
}, [courseUsageKey, sequenceId]);
return {
metadata,
units,
loaded,
};
}
export function useExamRedirect(metadata, blocks) {
useEffect(() => {
if (metadata !== null && blocks !== null) {
if (metadata.isTimeLimited) {
global.location.href = blocks[metadata.itemId].lmsWebUrl;
}
}
}, [metadata, blocks]);
}
/**
* Save the position of current unit the subsection
*/
export function usePersistentUnitPosition(courseUsageKey, sequenceId, unitId, sequenceMetadata) {
useEffect(() => {
// All values must be defined to function
const hasNeededData = courseUsageKey && sequenceId && unitId && sequenceMetadata;
if (!hasNeededData) {
return;
}
const { items, savePosition } = sequenceMetadata;
// A sub-section can individually specify whether positions should be saved
if (!savePosition) {
return;
}
const unitIndex = items.findIndex(({ id }) => unitId === id);
// "position" is a 1-indexed value due to legacy compatibility concerns.
// TODO: Make this value 0-indexed
const newPosition = unitIndex + 1;
// TODO: update the local understanding of the position and
// don't make requests to update the position if they still match?
saveSequencePosition(courseUsageKey, sequenceId, newPosition);
}, [courseUsageKey, sequenceId, unitId, sequenceMetadata]);
}
export function useMissingUnitRedirect(metadata, loaded) {
const { courseUsageKey, sequenceId, unitId } = useContext(CourseStructureContext);
useEffect(() => {
if (loaded && metadata.itemId === sequenceId && !unitId) {
// Position comes from the server as a 1-indexed array index. Convert it to 0-indexed.
const position = metadata.position - 1;
const nextUnitId = metadata.items[position].id;
history.push(`/course/${courseUsageKey}/${sequenceId}/${nextUnitId}`);
}
}, [loaded, metadata, unitId]);
}

View File

@@ -1,22 +0,0 @@
/* eslint-disable import/prefer-default-export */
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
import { getConfig } from '@edx/frontend-platform';
export async function getCourseBlocks(courseId, username) {
const url = new URL(`${getConfig().LMS_BASE_URL}/api/courses/v2/blocks/`);
url.searchParams.append('course_id', decodeURIComponent(courseId));
url.searchParams.append('username', username);
url.searchParams.append('depth', 3);
url.searchParams.append('requested_fields', 'children,show_gated_sections');
const { data } = await getAuthenticatedHttpClient().get(url.href, {});
return data;
}
export async function getCourse(courseId) {
const url = `${getConfig().LMS_BASE_URL}/api/courses/v2/courses/${courseId}`;
const { data } = await getAuthenticatedHttpClient().get(url);
return data;
}

View File

@@ -1,139 +0,0 @@
import { useContext, useMemo, useState, useEffect } from 'react';
import { history } from '@edx/frontend-platform';
import { AppContext } from '@edx/frontend-platform/react';
import CourseStructureContext from '../CourseStructureContext';
import { getCourseBlocks } from './api';
import { findBlockAncestry, createBlocksMap, createSequenceIdList, createUnitIdList } from './utils';
export function useBlockAncestry(blockId) {
const { blocks, loaded } = useContext(CourseStructureContext);
return useMemo(() => {
if (!loaded) {
return [];
}
return findBlockAncestry(
blocks,
blockId,
);
}, [blocks, blockId, loaded]);
}
export function useMissingSequenceRedirect(
loaded,
blocks,
courseUsageKey,
courseId,
sequenceId,
) {
useEffect(() => {
if (loaded && !sequenceId) {
const course = blocks[courseId];
const nextSectionId = course.children[0];
const nextSection = blocks[nextSectionId];
const nextSequenceId = nextSection.children[0];
const nextSequence = blocks[nextSequenceId];
const nextUnitId = nextSequence.children[0];
history.push(`/course/${courseUsageKey}/${nextSequenceId}/${nextUnitId}`);
}
}, [loaded, sequenceId]);
}
export function useLoadCourseStructure(courseUsageKey) {
const { authenticatedUser } = useContext(AppContext);
const [blocks, setBlocks] = useState(null);
const [loaded, setLoaded] = useState(false);
const [courseId, setCourseId] = useState();
useEffect(() => {
setLoaded(false);
getCourseBlocks(courseUsageKey, authenticatedUser.username).then((blocksData) => {
setBlocks(createBlocksMap(blocksData.blocks));
setCourseId(blocksData.root);
setLoaded(true);
});
// getCourse(courseUsageKey).then((courseData) => {
// });
}, [courseUsageKey]);
return {
blocks, loaded, courseId,
};
}
export function useCurrentCourse() {
const { loaded, courseId, blocks } = useContext(CourseStructureContext);
return loaded ? blocks[courseId] : null;
}
export function useCurrentSequence() {
const { loaded, blocks, sequenceId } = useContext(CourseStructureContext);
return loaded && sequenceId ? blocks[sequenceId] : null;
}
export function useCurrentSection() {
const { loaded, blocks } = useContext(CourseStructureContext);
const sequence = useCurrentSequence();
return loaded ? blocks[sequence.parentId] : null;
}
export function useCurrentUnit() {
const { loaded, blocks, unitId } = useContext(CourseStructureContext);
return loaded && unitId ? blocks[unitId] : null;
}
export function useUnitIds() {
const { loaded, blocks, courseId } = useContext(CourseStructureContext);
return useMemo(
() => (loaded ? createUnitIdList(blocks, courseId) : []),
[loaded, blocks, courseId],
);
}
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 useCurrentSequenceUnits() {
const { loaded, blocks } = useContext(CourseStructureContext);
const sequence = useCurrentSequence();
return loaded ? sequence.children.map(id => blocks[id]) : [];
}
export function useSequenceIdList() {
const { loaded, blocks, courseId } = useContext(CourseStructureContext);
const sequenceIdList = useMemo(
() => (loaded ? createSequenceIdList(blocks, courseId) : []),
[blocks, courseId],
);
return sequenceIdList;
}

View File

@@ -18,8 +18,6 @@ function Sequence({
units: initialUnits,
displayName,
showCompletion,
isTimeLimited,
bannerText,
onNext,
onPrevious,
onNavigateUnit,
@@ -79,7 +77,9 @@ function Sequence({
updateUnitCompletion(activeUnitId);
}
setActiveUnitId(newUnitId);
onNavigateUnit(newUnitId, units[newUnitId]);
if (onNavigateUnit !== null) {
onNavigateUnit(newUnitId, units[newUnitId]);
}
};
useEffect(() => {
@@ -133,7 +133,7 @@ Sequence.propTypes = {
bannerText: PropTypes.string,
onNext: PropTypes.func.isRequired,
onPrevious: PropTypes.func.isRequired,
onNavigateUnit: PropTypes.func.isRequired,
onNavigateUnit: PropTypes.func,
isGated: PropTypes.bool.isRequired,
prerequisite: PropTypes.shape({
name: PropTypes.string,
@@ -145,29 +145,7 @@ Sequence.propTypes = {
Sequence.defaultProps = {
bannerText: null,
onNavigateUnit: null,
};
export default injectIntl(Sequence);
// Sequence.propTypes = {
// id: PropTypes.string.isRequired,
// courseUsageKey: Pro
// unitIds: PropTypes.arrayOf(PropTypes.string).isRequired,
// units: PropTypes.objectOf(PropTypes.shape({
// })),
// displayName: PropTypes.string.isRequired,
// activeUnitId: PropTypes.string.isRequired,
// showCompletion: PropTypes.bool.isRequired,
// isTimeLimited: PropTypes.bool.isRequired,
// isGated: PropTypes.bool.isRequired,
// savePosition: PropTypes.bool.isRequired,
// bannerText: PropTypes.string,
// onNext: PropTypes.func.isRequired,
// onPrevious: PropTypes.func.isRequired,
// onNavigateUnit: PropTypes.func.isRequired,
// prerequisite: PropTypes.shape({
// name: PropTypes.string,
// id: PropTypes.string,
// }),
// };

View File

@@ -1,5 +0,0 @@
import React from 'react';
const SequenceMetadataContext = React.createContext({});
export default SequenceMetadataContext;

View File

@@ -1,65 +0,0 @@
import React, { useContext, Suspense } from 'react';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import SequenceNavigation from './SequenceNavigation';
import CourseStructureContext from '../CourseStructureContext';
import Unit from './Unit';
import {
useLoadSequenceMetadata,
useExamRedirect,
usePersistentUnitPosition,
useMissingUnitRedirect,
} from './data/hooks';
import SequenceMetadataContext from './SequenceMetadataContext';
import PageLoading from '../PageLoading';
import messages from './messages';
import { useCurrentUnit } from '../data/hooks';
const ContentLock = React.lazy(() => import('./content-lock'));
function Sequence({ intl }) {
const {
courseUsageKey,
sequenceId,
unitId,
blocks,
} = useContext(CourseStructureContext);
const { metadata, loaded } = useLoadSequenceMetadata(courseUsageKey, sequenceId);
usePersistentUnitPosition(courseUsageKey, sequenceId, unitId, metadata);
useExamRedirect(metadata, blocks);
useMissingUnitRedirect(metadata, loaded);
const unit = useCurrentUnit();
const ready = blocks !== null && metadata !== null && unitId && unit;
if (!ready) {
return null;
}
const isGated = metadata.gatedContent.gated;
return (
<SequenceMetadataContext.Provider value={metadata}>
<section className="d-flex flex-column flex-grow-1">
<SequenceNavigation />
{isGated && (
<Suspense fallback={<PageLoading
srMessage={intl.formatMessage(messages['learn.loading.content.lock'])}
/>}
>
<ContentLock />
</Suspense>
)}
{!isGated && <Unit id={unitId} unit={unit} />}
</section>
</SequenceMetadataContext.Provider>
);
}
Sequence.propTypes = {
intl: intlShape.isRequired,
};
export default injectIntl(Sequence);

View File

@@ -1,144 +0,0 @@
import React, { 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, faLock } from '@fortawesome/free-solid-svg-icons';
import { usePreviousUnit, useNextUnit, useCurrentSequenceUnits, useCurrentUnit } from '../data/hooks';
import CourseStructureContext from '../CourseStructureContext';
import SequenceMetadataContext from './SequenceMetadataContext';
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;
case 'lock':
icon = faLock;
break;
default:
icon = faBook;
}
return (
<FontAwesomeIcon icon={icon} />
);
}
UnitIcon.propTypes = {
type: PropTypes.oneOf(['video', 'other', 'vertical', 'problem', 'lock']).isRequired,
};
export default function SequenceNavigation() {
const { courseUsageKey, unitId } = useContext(CourseStructureContext);
const previousUnit = usePreviousUnit();
const nextUnit = useNextUnit();
const handlePreviousClick = useCallback(() => {
if (previousUnit) {
history.push(`/course/${courseUsageKey}/${previousUnit.parentId}/${previousUnit.id}`);
}
});
const handleNextClick = useCallback(() => {
if (nextUnit) {
history.push(`/course/${courseUsageKey}/${nextUnit.parentId}/${nextUnit.id}`);
}
});
const handleUnitClick = useCallback((unit) => {
history.push(`/course/${courseUsageKey}/${unit.parentId}/${unit.id}`);
});
if (!unitId) {
return null;
}
return (
<nav className="flex-grow-0 d-flex w-100 mb-3">
<Button
key="previous"
className="btn-outline-primary"
onClick={handlePreviousClick}
>
Previous
</Button>
<UnitNavigation clickHandler={handleUnitClick} />
<Button
key="next"
className="btn-outline-primary"
onClick={handleNextClick}
>
Next
</Button>
</nav>
);
}
function UnitNavigation({ clickHandler }) {
const currentUnit = useCurrentUnit();
const units = useCurrentSequenceUnits();
const metadata = useContext(SequenceMetadataContext);
const isGated = metadata.gatedContent.gated;
return (
<div className="btn-group ml-2 mr-2 flex-grow-1 d-flex" role="group">
{!isGated && units.map(unit => (
<UnitButton key={unit.id} unit={unit} disabled={unit.id === currentUnit.id} clickHandler={clickHandler} />
))}
{isGated && <UnitButton key={currentUnit.id} unit={currentUnit} disabled locked />}
</div>
);
}
UnitNavigation.propTypes = {
clickHandler: PropTypes.func.isRequired,
};
function UnitButton({
unit, disabled, locked, clickHandler,
}) {
const { id, type } = unit;
const handleClick = useCallback(() => {
if (clickHandler !== null) {
clickHandler(unit);
}
}, [unit]);
return (
<Button
key={id}
className="btn-outline-secondary unit-button flex-grow-1"
onClick={handleClick}
disabled={disabled}
>
<UnitIcon type={locked ? 'lock' : type} />
</Button>
);
}
UnitButton.propTypes = {
unit: PropTypes.shape({
id: PropTypes.string.isRequired,
type: PropTypes.oneOf(['video', 'other', 'vertical', 'problem']).isRequired,
}).isRequired,
disabled: PropTypes.bool.isRequired, // Whether or not the button will function.
locked: PropTypes.bool, // Whether the unit is semantically "locked" and unnavigable.
clickHandler: PropTypes.func,
};
UnitButton.defaultProps = {
clickHandler: null,
locked: false,
};

View File

@@ -1,24 +0,0 @@
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
className="flex-grow-1"
title={displayName}
ref={iframeRef}
src={iframeUrl}
/>
);
}
Unit.propTypes = {
id: PropTypes.string.isRequired,
unit: PropTypes.shape({
displayName: PropTypes.string.isRequired,
}).isRequired,
};