AA-181: Outline Tab Refactor (#80)

- Updated the Outline Tab to fetch course blocks from the Outline API.
- Changed naming conventions to more accurately portray the tab naming scheme
(ex. Outline Tab, Dates Tab, etc.)
- Removed logic from `fetchCourses` that was specific to the Outline Tab
This commit is contained in:
Carla Duarte
2020-06-15 15:19:13 -04:00
committed by GitHub
parent 65173e9f93
commit 253836fa9f
9 changed files with 75 additions and 58 deletions

View File

@@ -6,9 +6,13 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import SequenceLink from './SequenceLink';
import { useModel } from '../model-store';
export default function Section({ id, courseId }) {
const section = useModel('sections', id);
const { title, sequenceIds } = section;
export default function Section({ courseId, title, sequenceIds }) {
const {
courseBlocks: {
sequences,
},
} = useModel('outline', courseId);
return (
<Collapsible.Advanced className="collapsible-card mb-2">
<Collapsible.Trigger className="collapsible-trigger d-flex align-items-start">
@@ -31,6 +35,7 @@ export default function Section({ id, courseId }) {
key={sequenceId}
id={sequenceId}
courseId={courseId}
title={sequences[sequenceId].title}
/>
))}
</Collapsible.Body>
@@ -39,6 +44,7 @@ export default function Section({ id, courseId }) {
}
Section.propTypes = {
id: PropTypes.string.isRequired,
courseId: PropTypes.string.isRequired,
title: PropTypes.string.isRequired,
sequenceIds: PropTypes.arrayOf(PropTypes.string).isRequired,
};

View File

@@ -1,13 +1,11 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Link } from 'react-router-dom';
import { useModel } from '../model-store';
export default function SequenceLink({ id, courseId }) {
const sequence = useModel('sequences', id);
export default function SequenceLink({ id, courseId, title }) {
return (
<div className="ml-4">
<Link to={`/course/${courseId}/${id}`}>{sequence.title}</Link>
<Link to={`/course/${courseId}/${id}`}>{title}</Link>
</div>
);
}
@@ -15,4 +13,5 @@ export default function SequenceLink({ id, courseId }) {
SequenceLink.propTypes = {
id: PropTypes.string.isRequired,
courseId: PropTypes.string.isRequired,
title: PropTypes.string.isRequired,
};

View File

@@ -1 +1 @@
export { default } from './CourseHome';
export { default } from './outline-tab/OutlineTab';

View File

@@ -2,21 +2,21 @@ import React from 'react';
import { useSelector } from 'react-redux';
import { Button } from '@edx/paragon';
import { AlertList } from '../user-messages';
import { AlertList } from '../../user-messages';
import CourseDates from './CourseDates';
import CourseTools from './CourseTools';
import Section from './Section';
import { useModel } from '../model-store';
import CourseDates from '../CourseDates';
import CourseTools from '../CourseTools';
import Section from '../Section';
import { useModel } from '../../model-store';
// Note that we import from the component files themselves in the enrollment-alert package.
// This is because React.lazy() requires that we import() from a file with a Component as its
// default export.
// See React.lazy docs here: https://reactjs.org/docs/code-splitting.html#reactlazy
const { EnrollmentAlert, StaffEnrollmentAlert } = React.lazy(() => import('../alerts/enrollment-alert'));
const LogistrationAlert = React.lazy(() => import('../alerts/logistration-alert'));
const { EnrollmentAlert, StaffEnrollmentAlert } = React.lazy(() => import('../../alerts/enrollment-alert'));
const LogistrationAlert = React.lazy(() => import('../../alerts/logistration-alert'));
export default function CourseHome() {
export default function OutlineTab() {
const {
courseId,
} = useSelector(state => state.courseware);
@@ -29,9 +29,18 @@ export default function CourseHome() {
enrollmentEnd,
enrollmentMode,
isEnrolled,
sectionIds,
} = useModel('courses', courseId);
const {
courseBlocks: {
courses,
sections,
},
} = useModel('outline', courseId);
const rootCourseId = Object.keys(courses)[0];
const { sectionIds } = courses[rootCourseId];
return (
<>
<AlertList
@@ -52,8 +61,9 @@ export default function CourseHome() {
{sectionIds.map((sectionId) => (
<Section
key={sectionId}
id={sectionId}
courseId={courseId}
title={sections[sectionId].title}
sequenceIds={sections[sectionId].sequenceIds}
/>
))}
</div>

View File

@@ -46,8 +46,8 @@ export async function getCourseMetadata(courseId) {
return normalizeMetadata(data);
}
export async function getTabData(courseId, tab, version) {
const url = `${getConfig().LMS_BASE_URL}/api/course_home/${version}/${tab}/${courseId}`;
export async function getDatesTabData(courseId, version) {
const url = `${getConfig().LMS_BASE_URL}/api/course_home/${version}/dates/${courseId}`;
try {
const { data } = await getAuthenticatedHttpClient().get(url);
return camelCaseObject(data);
@@ -63,17 +63,6 @@ export async function getTabData(courseId, tab, version) {
}
}
function normalizeOutlineTabData(courseId, courseToolData) {
const courseTools = camelCaseObject(courseToolData);
return { id: courseId, courseTools };
}
export async function getOutlineTabData(courseId) {
const url = `${getConfig().LMS_BASE_URL}/api/course_home/v1/outline/${courseId}`;
const { data } = await getAuthenticatedHttpClient().get(url, {});
return normalizeOutlineTabData(courseId, data.course_tools);
}
function normalizeBlocks(courseId, blocks) {
const models = {
courses: {},
@@ -161,6 +150,26 @@ export async function getCourseBlocks(courseId) {
return normalizeBlocks(courseId, data.blocks);
}
export async function getOutlineTabData(courseId, version) {
const url = `${getConfig().LMS_BASE_URL}/api/course_home/${version}/outline/${courseId}`;
let { tabData } = {};
try {
tabData = await getAuthenticatedHttpClient().get(url);
} catch (error) {
const { httpErrorStatus } = error && error.customAttributes;
if (httpErrorStatus === 404) {
return window.location.replace(`${getConfig().LMS_BASE_URL}/courses/${courseId}/home`);
}
}
const {
data,
} = tabData;
const courseBlocks = normalizeBlocks(courseId, data.course_blocks.blocks);
const courseTools = camelCaseObject(data.course_tools);
return { courseTools, courseBlocks };
}
function normalizeSequenceMetadata(sequence) {
return {

View File

@@ -1,6 +1,7 @@
export {
fetchCourse,
fetchDatesTab,
fetchOutlineTab,
fetchSequence,
} from './thunks';

View File

@@ -3,7 +3,7 @@ import {
getCourseMetadata,
getCourseBlocks,
getSequenceMetadata,
getTabData,
getDatesTabData,
getOutlineTabData,
} from './api';
import {
@@ -28,8 +28,7 @@ export function fetchCourse(courseId) {
Promise.allSettled([
getCourseMetadata(courseId),
getCourseBlocks(courseId),
getOutlineTabData(courseId),
]).then(([courseMetadataResult, courseBlocksResult, outlineTabResult]) => {
]).then(([courseMetadataResult, courseBlocksResult]) => {
if (courseMetadataResult.status === 'fulfilled') {
dispatch(addModel({
modelType: 'courses',
@@ -62,16 +61,8 @@ export function fetchCourse(courseId) {
}));
}
if (outlineTabResult.status === 'fulfilled') {
dispatch(addModel({
modelType: 'outline',
model: outlineTabResult.value,
}));
}
const fetchedMetadata = courseMetadataResult.status === 'fulfilled';
const fetchedBlocks = courseBlocksResult.status === 'fulfilled';
const fetchedOutline = outlineTabResult.status === 'fulfilled';
// Log errors for each request if needed. Course block failures may occur
// even if the course metadata request is successful
@@ -81,18 +72,15 @@ export function fetchCourse(courseId) {
if (!fetchedMetadata) {
logError(courseMetadataResult.reason);
}
if (!fetchedOutline) {
logError(outlineTabResult.reason);
}
if (fetchedMetadata) {
if (courseMetadataResult.value.canLoadCourseware.hasAccess && fetchedBlocks && fetchedOutline) {
if (courseMetadataResult.value.canLoadCourseware.hasAccess && fetchedBlocks) {
// User has access
dispatch(fetchCourseSuccess({ courseId }));
return;
}
// User either doesn't have access or only has partial access
// (can't access course blocks or course outline)
// (can't access course blocks)
dispatch(fetchCourseDenied({ courseId }));
return;
}
@@ -103,12 +91,12 @@ export function fetchCourse(courseId) {
};
}
export function fetchTab(courseId, tab, version) {
export function fetchTab(courseId, tab, version, getTabData) {
return async (dispatch) => {
dispatch(fetchTabRequest({ courseId }));
Promise.allSettled([
getCourseMetadata(courseId),
getTabData(courseId, tab, version),
getTabData(courseId, version),
]).then(([courseMetadataResult, tabDataResult]) => {
const fetchedMetadata = courseMetadataResult.status === 'fulfilled';
const fetchedTabData = tabDataResult.status === 'fulfilled';
@@ -149,7 +137,11 @@ export function fetchTab(courseId, tab, version) {
}
export function fetchDatesTab(courseId) {
return fetchTab(courseId, 'dates', 'v1');
return fetchTab(courseId, 'dates', 'v1', getDatesTabData);
}
export function fetchOutlineTab(courseId) {
return fetchTab(courseId, 'outline', 'v1', getOutlineTabData);
}
export function fetchSequence(sequenceId) {

View File

@@ -1,10 +1,10 @@
// Sample data helpful when developing, to see a variety of configurations.
// This set of data is not realistic (mix of having access and not), but it
// is intended to demonstrate many UI results.
// To use, have getTabData in api.js return the result of this call instead:
// To use, have getDatesTabData in api.js return the result of this call instead:
/*
import fakeDatesData from '../dates-tab/fakeData';
export async function getTabData(courseId, tab, version) {
export async function getDatesTabData(courseId, version) {
if (tab === 'dates') { return camelCaseObject(fakeDatesData()); }
...
}

View File

@@ -18,14 +18,14 @@ import { UserMessagesProvider } from './user-messages';
import './index.scss';
import './assets/favicon.ico';
import CourseHome from './course-home';
import OutlineTab from './course-home';
import CoursewareContainer from './courseware';
import CoursewareRedirect from './CoursewareRedirect';
import DatesTab from './dates-tab';
import { TabContainer } from './tab-page';
import store from './store';
import { fetchCourse, fetchDatesTab } from './data';
import { fetchDatesTab, fetchOutlineTab } from './data';
subscribe(APP_READY, () => {
ReactDOM.render(
@@ -33,9 +33,9 @@ subscribe(APP_READY, () => {
<UserMessagesProvider>
<Switch>
<Route path="/redirect" component={CoursewareRedirect} />
<Route path="/course/:courseId/home">
<TabContainer tab="courseware" fetch={fetchCourse}>
<CourseHome />
<Route path="/course/:courseId/outline">
<TabContainer tab="outline" fetch={fetchOutlineTab}>
<OutlineTab />
</TabContainer>
</Route>
<Route path="/course/:courseId/dates">