Compare commits

...

1 Commits

Author SHA1 Message Date
Adam Butterworth
93a569f9ec fix: initial sequence component set 2020-01-14 15:32:58 -05:00
12 changed files with 545 additions and 1 deletions

View File

@@ -9,6 +9,7 @@ import CourseStructureContext from './CourseStructureContext';
import { useLoadCourseStructure, useMissingSubSectionRedirect } from './data/hooks';
import SubSection from './sub-section/SubSection';
import { history } from '@edx/frontend-platform';
import Sequence from './sequence/Sequence';
function LearningSequencePage({ match, intl }) {
const {
@@ -37,7 +38,163 @@ function LearningSequencePage({ match, intl }) {
/>}
{loaded && unitId && <CourseBreadcrumbs />}
{subSectionId && <SubSection />}
{subSectionId && (
<Sequence
courseId={courseId}
id={subSectionId}
unitIds={[
"block-v1:edX+DemoX+Demo_Course+type@vertical+block@867dddb6f55d410caaa9c1eb9c6743ec",
"block-v1:edX+DemoX+Demo_Course+type@vertical+block@3e3f9b5199ba4e96b2fc6539087cfe2c",
"block-v1:edX+DemoX+Demo_Course+type@vertical+block@4f6c1b4e316a419ab5b6bf30e6c708e9",
"block-v1:edX+DemoX+Demo_Course+type@vertical+block@3dc16db8d14842e38324e95d4030b8a0",
"block-v1:edX+DemoX+Demo_Course+type@vertical+block@4a1bba2a403f40bca5ec245e945b0d76",
"block-v1:edX+DemoX+Demo_Course+type@vertical+block@f0e6d90842c44cc7a50fd1a18a7dd982",
"block-v1:edX+DemoX+Demo_Course+type@vertical+block@256f17a44983429fb1a60802203ee4e0",
"block-v1:edX+DemoX+Demo_Course+type@vertical+block@e3601c0abee6427d8c17e6d6f8fdddd1",
"block-v1:edX+DemoX+Demo_Course+type@vertical+block@a79d59cd72034188a71d388f4954a606",
"block-v1:edX+DemoX+Demo_Course+type@vertical+block@134df56c516a4a0dbb24dd5facef746e",
"block-v1:edX+DemoX+Demo_Course+type@vertical+block@d91b9e5d8bc64d57a1332d06bf2f2193",
]}
units={{
"block-v1:edX+DemoX+Demo_Course+type@vertical+block@867dddb6f55d410caaa9c1eb9c6743ec": {
"type": "other",
"path": "Example Week 1: Getting Started > Lesson 1 - Getting Started > Getting Started",
"bookmarked": false,
"graded": false,
"page_title": "Getting Started",
"href": "",
"complete": true,
"content": "",
"id": "block-v1:edX+DemoX+Demo_Course+type@vertical+block@867dddb6f55d410caaa9c1eb9c6743ec"
},
"block-v1:edX+DemoX+Demo_Course+type@vertical+block@3e3f9b5199ba4e96b2fc6539087cfe2c": {
"type": "other",
"path": "Example Week 1: Getting Started > Lesson 1 - Getting Started > MY unit",
"bookmarked": false,
"graded": false,
"page_title": "MY unit",
"href": "",
"complete": false,
"content": "",
"id": "block-v1:edX+DemoX+Demo_Course+type@vertical+block@3e3f9b5199ba4e96b2fc6539087cfe2c"
},
"block-v1:edX+DemoX+Demo_Course+type@vertical+block@4f6c1b4e316a419ab5b6bf30e6c708e9": {
"type": "video",
"path": "Example Week 1: Getting Started > Lesson 1 - Getting Started > Working with Videos",
"bookmarked": false,
"graded": false,
"page_title": "Working with Videos",
"href": "",
"complete": false,
"content": "",
"id": "block-v1:edX+DemoX+Demo_Course+type@vertical+block@4f6c1b4e316a419ab5b6bf30e6c708e9"
},
"block-v1:edX+DemoX+Demo_Course+type@vertical+block@3dc16db8d14842e38324e95d4030b8a0": {
"type": "video",
"path": "Example Week 1: Getting Started > Lesson 1 - Getting Started > Videos on edX",
"bookmarked": false,
"graded": false,
"page_title": "Videos on edX",
"href": "",
"complete": false,
"content": "",
"id": "block-v1:edX+DemoX+Demo_Course+type@vertical+block@3dc16db8d14842e38324e95d4030b8a0"
},
"block-v1:edX+DemoX+Demo_Course+type@vertical+block@4a1bba2a403f40bca5ec245e945b0d76": {
"type": "other",
"path": "Example Week 1: Getting Started > Lesson 1 - Getting Started > Video Demonstrations",
"bookmarked": false,
"graded": false,
"page_title": "Video Demonstrations",
"href": "",
"complete": false,
"content": "",
"id": "block-v1:edX+DemoX+Demo_Course+type@vertical+block@4a1bba2a403f40bca5ec245e945b0d76"
},
"block-v1:edX+DemoX+Demo_Course+type@vertical+block@f0e6d90842c44cc7a50fd1a18a7dd982": {
"type": "video",
"path": "Example Week 1: Getting Started > Lesson 1 - Getting Started > Video Demonstrations",
"bookmarked": false,
"graded": false,
"page_title": "Video Demonstrations",
"href": "",
"complete": false,
"content": "",
"id": "block-v1:edX+DemoX+Demo_Course+type@vertical+block@f0e6d90842c44cc7a50fd1a18a7dd982"
},
"block-v1:edX+DemoX+Demo_Course+type@vertical+block@256f17a44983429fb1a60802203ee4e0": {
"type": "video",
"path": "Example Week 1: Getting Started > Lesson 1 - Getting Started > Video Presentation Styles",
"bookmarked": false,
"graded": false,
"page_title": "Video Presentation Styles",
"href": "",
"complete": false,
"content": "",
"id": "block-v1:edX+DemoX+Demo_Course+type@vertical+block@256f17a44983429fb1a60802203ee4e0"
},
"block-v1:edX+DemoX+Demo_Course+type@vertical+block@e3601c0abee6427d8c17e6d6f8fdddd1": {
"type": "problem",
"path": "Example Week 1: Getting Started > Lesson 1 - Getting Started > Interactive Questions",
"bookmarked": false,
"graded": false,
"page_title": "Interactive Questions",
"href": "",
"complete": false,
"content": "",
"id": "block-v1:edX+DemoX+Demo_Course+type@vertical+block@e3601c0abee6427d8c17e6d6f8fdddd1"
},
"block-v1:edX+DemoX+Demo_Course+type@vertical+block@a79d59cd72034188a71d388f4954a606": {
"type": "other",
"path": "Example Week 1: Getting Started > Lesson 1 - Getting Started > Exciting Labs and Tools",
"bookmarked": false,
"graded": false,
"page_title": "Exciting Labs and Tools",
"href": "",
"complete": false,
"content": "",
"id": "block-v1:edX+DemoX+Demo_Course+type@vertical+block@a79d59cd72034188a71d388f4954a606"
},
"block-v1:edX+DemoX+Demo_Course+type@vertical+block@134df56c516a4a0dbb24dd5facef746e": {
"type": "problem",
"path": "Example Week 1: Getting Started > Lesson 1 - Getting Started > Reading Assignments",
"bookmarked": false,
"graded": false,
"page_title": "Reading Assignments",
"href": "",
"complete": false,
"content": "",
"id": "block-v1:edX+DemoX+Demo_Course+type@vertical+block@134df56c516a4a0dbb24dd5facef746e"
},
"block-v1:edX+DemoX+Demo_Course+type@vertical+block@d91b9e5d8bc64d57a1332d06bf2f2193": {
"type": "other",
"path": "Example Week 1: Getting Started > Lesson 1 - Getting Started > When Are Your Exams? ",
"bookmarked": false,
"graded": false,
"page_title": "When Are Your Exams? ",
"href": "",
"complete": false,
"content": "",
"id": "block-v1:edX+DemoX+Demo_Course+type@vertical+block@d91b9e5d8bc64d57a1332d06bf2f2193"
},
}}
displayName="Sequence Name"
activeUnitId="block-v1:edX+DemoX+Demo_Course+type@vertical+block@867dddb6f55d410caaa9c1eb9c6743ec"
showCompletion={true}
isTimeLimited={false}
bannerText={null}
onNext={() => {}}
onPrevious={() => {}}
onNavigateUnit={() => {}}
isGated={false}
prerequisite={{
name: 'Prerequisite name',
url: 'url? or id',
id: 'asdasd',
}}
savePosition={true}
/>
)}
</CourseStructureContext.Provider>
</main>

View File

@@ -0,0 +1,7 @@
import React from 'react';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faCheckCircle } from '@fortawesome/free-solid-svg-icons';
export default function CompleteIcon(props) {
return <FontAwesomeIcon icon={faCheckCircle} {...props} />
}

View File

@@ -0,0 +1,139 @@
import React, { useState, useEffect, Suspense } from 'react';
import PropTypes from 'prop-types';
import Unit from './Unit';
import SequenceNavigation from './SequenceNavigation';
import PageLoading from '../PageLoading';
import { getBlockCompletion, saveSequencePosition } from './api';
const ContentLock = React.lazy(() => import('./content-lock'));
import messages from './messages';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
function Sequence({
courseId,
id,
unitIds,
units: initialUnits,
displayName,
showCompletion,
isTimeLimited,
bannerText,
onNext,
onPrevious,
onNavigateUnit,
isGated,
prerequisite,
savePosition,
activeUnitId: initialActiveUnitId,
intl,
}) {
const [units, setUnits] = useState(initialUnits);
const [activeUnitId, setActiveUnitId] = useState(initialActiveUnitId);
const activeUnitIndex = unitIds.indexOf(activeUnitId);
const activeUnit = units[activeUnitId];
const unitsArr = unitIds.map((unitId) => ({
...units[unitId],
id: unitId,
isActive: unitId === activeUnitId,
}));
const updateUnitCompletion = (unitId) => {
// If the unit is already complete, don't check.
if (units[unitId].complete) {
return;
}
getBlockCompletion(courseId, id, unitId).then((isComplete) => {
if (isComplete) {
setUnits({
...units,
[unitId]: { ...units[unitId], complete: isComplete },
});
}
})
};
const handleNext = () => {
if (activeUnitIndex < unitIds.length - 1) {
handleNavigate(activeUnitIndex + 1);
} else {
onNext();
}
};
const handlePrevious = () => {
if (activeUnitIndex > 0) {
handleNavigate(activeUnitIndex - 1);
} else {
onPrevious();
}
};
const handleNavigate = (unitIndex) => {
const newUnitId = unitIds[unitIndex];
if (showCompletion) {
updateUnitCompletion(activeUnitId);
}
setActiveUnitId(newUnitId);
onNavigateUnit(newUnitId, units[newUnitId]);
};
useEffect(() => {
if (savePosition) {
saveSequencePosition(courseId, id, activeUnitIndex);
}
}, [activeUnitId]);
return (
<div className="d-flex flex-column flex-grow-1">
<SequenceNavigation
onNext={handleNext}
onNavigate={handleNavigate}
onPrevious={handlePrevious}
units={unitsArr}
isLocked={isGated}
showCompletion={showCompletion}
/>
{isGated? (
<Suspense fallback={<PageLoading
srMessage={intl.formatMessage(messages['learn.loading.content.lock'])}
/>}>
<ContentLock
courseId={courseId}
sectionName={displayName}
prereqSectionName={prerequisite.name}
prereqId={prerequisite.id}
/>
</Suspense>
): (
<Unit key={activeUnitId} {...activeUnit} />
)}
</div>
);
}
Sequence.propTypes = {
id: PropTypes.string.isRequired,
courseId: PropTypes.string.isRequired,
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,
bannerText: PropTypes.string,
onNext: PropTypes.func.isRequired,
onPrevious: PropTypes.func.isRequired,
onNavigateUnit: PropTypes.func.isRequired,
isGated: PropTypes.bool.isRequired,
prerequisite: PropTypes.shape({
name: PropTypes.string.isRequired,
url: PropTypes.string.isRequired,
id: PropTypes.string.isRequired,
}),
savePosition: PropTypes.bool.isRequired,
intl: intlShape.isRequired,
};
export default injectIntl(Sequence);

View File

@@ -0,0 +1,35 @@
import React from 'react';
import { Button } from '@edx/paragon';
import UnitButton from './UnitButton';
export default function SequenceNavigation({
onNext,
onPrevious,
onNavigate,
units,
isLocked,
showCompletion,
}) {
const unitButtons = units.map((unit, index) => (
<UnitButton
key={unit.id}
{...unit}
isComplete={showCompletion && unit.complete}
onClick={onNavigate.bind(null, index)}
/>
));
return (
<nav className="flex-grow-0 d-flex w-100 mb-3 btn-group">
<Button className="btn-outline-primary" onClick={onPrevious}>
Previous
</Button>
{isLocked ? <UnitButton type="lock" isActive={true} /> : unitButtons}
<Button className="btn-outline-primary" onClick={onNext}>
Next
</Button>
</nav>
);
}

View File

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

View File

@@ -0,0 +1,29 @@
import React from 'react';
import classNames from 'classnames';
import { Button } from '@edx/paragon';
import UnitIcon from './UnitIcon';
import CompleteIcon from './CompleteIcon';
export default function UnitButton({
onClick,
title,
type,
isActive,
isComplete,
}) {
return (
<Button
className={classNames({
active: isActive,
"btn-outline-primary": !isActive,
"btn-outline-secondary": isActive,
})}
disabled={isActive}
onClick={onClick}
title={title}
>
<UnitIcon type={type} />
{isComplete ? <CompleteIcon className="text-success ml-2" /> : null}
</Button>
)
}

View File

@@ -0,0 +1,35 @@
import React from 'react';
import PropTypes from 'prop-types';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faFilm, faBook, faPencilAlt, faTasks, faLock } from '@fortawesome/free-solid-svg-icons';
export default 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,
};

View File

@@ -0,0 +1,50 @@
import { getConfig } from '@edx/frontend-platform';
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
/* eslint-disable import/prefer-default-export */
const getSequenceXModuleHandlerUrl = (courseId, sequenceId) =>
`${getConfig().LMS_BASE_URL}/courses/${courseId}/xblock/${sequenceId}/handler/xmodule_handler`;
export async function saveSequencePosition(courseId, 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 + 1);
const requestConfig = {
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
};
const { data } = await getAuthenticatedHttpClient().post(
`${getSequenceXModuleHandlerUrl(courseId, sequenceId)}/goto_position`,
urlEncoded.toString(),
requestConfig,
);
return data;
}
export async function getBlockCompletion(courseId, sequenceId, usageKey) {
// 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('usage_key', usageKey);
const requestConfig = {
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
};
const { data } = await getAuthenticatedHttpClient().post(
`${getSequenceXModuleHandlerUrl(courseId, sequenceId)}/get_completion`,
urlEncoded.toString(),
requestConfig,
);
if (data.complete) {
return true;
}
return false;
}

View File

@@ -0,0 +1,37 @@
import React, { useContext, useCallback } from 'react';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faLock } from '@fortawesome/free-solid-svg-icons';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { history } from '@edx/frontend-platform';
import { Button } from '@edx/paragon';
import messages from './messages';
function ContentLock({ intl, courseId, prereqSectionName, prereqId, sectionName }) {
const handleClick = useCallback(() => {
history.push(`/course/${courseId}/${prereqId}`);
});
return (
<>
<h3>
<FontAwesomeIcon icon={faLock} />{' '}
{sectionName}
</h3>
<h4>{intl.formatMessage(messages['learn.contentLock.content.locked'])}</h4>
<p>{intl.formatMessage(messages['learn.contentLock.complete.prerequisite'], {
prereqSectionName,
})}
</p>
<p>
<Button className="btn-primary" onClick={handleClick}>{intl.formatMessage(messages['learn.contentLock.goToSection'])}</Button>
</p>
</>
);
}
ContentLock.propTypes = {
intl: intlShape.isRequired,
};
export default injectIntl(ContentLock);

View File

@@ -0,0 +1 @@
export { default } from './ContentLock';

View File

@@ -0,0 +1,21 @@
import { defineMessages } from '@edx/frontend-platform/i18n';
const messages = defineMessages({
'learn.contentLock.content.locked': {
id: 'learn.contentLock.content.locked',
defaultMessage: 'Content Locked',
description: 'Message shown to indicate that a piece of content is unavailable and has a prerequisite.',
},
'learn.contentLock.complete.prerequisite': {
id: 'learn.contentLock.complete.prerequisite',
defaultMessage: "You must complete the prerequisite: '{prereqSectionName}' to access this content.",
description: 'Message shown to indicate which prerequisite the student must complete prior to accessing the locked content. {prereqSectionName} is the name of the prerequisite.',
},
'learn.contentLock.goToSection': {
id: 'learn.contentLock.goToSection',
defaultMessage: 'Go To Prerequisite Section',
description: 'A button users can click that navigates their browser to the prerequisite of this section.',
},
});
export default messages;

View File

@@ -0,0 +1,11 @@
import { defineMessages } from '@edx/frontend-platform/i18n';
const messages = defineMessages({
'learn.loading.content.lock': {
id: 'learn.loading.content.lock',
defaultMessage: 'Loading locked content messaging...',
description: 'Message shown when an interface about locked content is being loaded',
},
});
export default messages;