Unverified Commit 2f01e8a6 authored by Michael Terry's avatar Michael Terry Committed by GitHub
Browse files

Refactor containers to share more code (#61)

Specifically, make sure that the header, footer, and tabs are all
shared code so that they look the same and don't need to be
redefined as we add more tab pages.
parent 589db935
File mode changed from 100755 to 100644
File mode changed from 100755 to 100644
import React from 'react';
import PropTypes from 'prop-types';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { getConfig } from '@edx/frontend-platform';
import classNames from 'classnames';
import messages from './messages';
import Tabs from '../tabs/Tabs';
function CourseTabsNavigation({
activeTabSlug, tabs, intl,
activeTabSlug, className, tabs, intl,
}) {
return (
<div className="course-tabs-navigation">
<div className={classNames('course-tabs-navigation', className)}>
<div className="container-fluid">
<Tabs
className="nav-underline-tabs"
......@@ -21,7 +20,7 @@ function CourseTabsNavigation({
<a
key={slug}
className={classNames('nav-item flex-shrink-0 nav-link', { active: slug === activeTabSlug })}
href={`${getConfig().LMS_BASE_URL}${url}`}
href={url}
>
{title}
</a>
......@@ -34,6 +33,7 @@ function CourseTabsNavigation({
CourseTabsNavigation.propTypes = {
activeTabSlug: PropTypes.string,
className: PropTypes.string,
tabs: PropTypes.arrayOf(PropTypes.shape({
title: PropTypes.string.isRequired,
priority: PropTypes.number.isRequired,
......@@ -45,6 +45,7 @@ CourseTabsNavigation.propTypes = {
CourseTabsNavigation.defaultProps = {
activeTabSlug: undefined,
className: null,
};
export default injectIntl(CourseTabsNavigation);
......@@ -68,7 +68,13 @@ export default function Header({
}
Header.propTypes = {
courseOrg: PropTypes.string.isRequired,
courseNumber: PropTypes.string.isRequired,
courseTitle: PropTypes.string.isRequired,
courseOrg: PropTypes.string,
courseNumber: PropTypes.string,
courseTitle: PropTypes.string,
};
Header.defaultProps = {
courseOrg: null,
courseNumber: null,
courseTitle: null,
};
import React from 'react';
import PropTypes from 'prop-types';
import { useSelector } from 'react-redux';
import { Button } from '@edx/paragon';
import { AlertList } from '../user-messages';
import { Header, CourseTabsNavigation } from '../course-header';
import { useLogistrationAlert } from '../logistration-alert';
import { useEnrollmentAlert } from '../enrollment-alert';
import CourseDates from './CourseDates';
import Section from './Section';
......@@ -18,15 +15,12 @@ import { useModel } from '../model-store';
const { EnrollmentAlert, StaffEnrollmentAlert } = React.lazy(() => import('../enrollment-alert'));
const LogistrationAlert = React.lazy(() => import('../logistration-alert'));
export default function CourseHome({
courseId,
}) {
useLogistrationAlert();
useEnrollmentAlert(courseId);
export default function CourseHome() {
const {
courseId,
} = useSelector(state => state.courseware);
const {
org,
number,
title,
start,
end,
......@@ -34,64 +28,45 @@ export default function CourseHome({
enrollmentEnd,
enrollmentMode,
isEnrolled,
tabs,
sectionIds,
} = useModel('courses', courseId);
return (
<>
<Header
courseOrg={org}
courseNumber={number}
courseTitle={title}
<AlertList
topic="outline"
className="mb-3"
customAlerts={{
clientEnrollmentAlert: EnrollmentAlert,
clientStaffEnrollmentAlert: StaffEnrollmentAlert,
clientLogistrationAlert: LogistrationAlert,
}}
/>
<main className="d-flex flex-column flex-grow-1">
<div className="container-fluid">
<CourseTabsNavigation tabs={tabs} className="mb-3" activeTabSlug="courseware" />
<AlertList
topic="outline"
className="mb-3"
customAlerts={{
clientEnrollmentAlert: EnrollmentAlert,
clientStaffEnrollmentAlert: StaffEnrollmentAlert,
clientLogistrationAlert: LogistrationAlert,
}}
/>
<div className="d-flex justify-content-between mb-3">
<h2>{title}</h2>
<Button className="btn-primary" type="button">Resume Course</Button>
</div>
<div className="row">
<div className="col col-8">
{sectionIds.map((sectionId) => (
<Section
key={sectionId}
id={sectionId}
courseId={courseId}
/>
))}
</div>
<div className="flex-grow-1">
<div className="container-fluid">
<div className="d-flex justify-content-between mb-3">
<h2>{title}</h2>
<Button className="btn-primary" type="button">Resume Course</Button>
</div>
<div className="row">
<div className="col col-8">
{sectionIds.map((sectionId) => (
<Section
key={sectionId}
id={sectionId}
courseId={courseId}
/>
))}
</div>
<div className="col col-4">
<CourseDates
start={start}
end={end}
enrollmentStart={enrollmentStart}
enrollmentEnd={enrollmentEnd}
enrollmentMode={enrollmentMode}
isEnrolled={isEnrolled}
/>
</div>
</div>
</div>
<div className="col col-4">
<CourseDates
start={start}
end={end}
enrollmentStart={enrollmentStart}
enrollmentEnd={enrollmentEnd}
enrollmentMode={enrollmentMode}
isEnrolled={isEnrolled}
/>
</div>
</main>
</div>
</>
);
}
CourseHome.propTypes = {
courseId: PropTypes.string.isRequired,
};
export { default } from './CourseHomeContainer';
export { default } from './CourseHome';
import { defineMessages } from '@edx/frontend-platform/i18n';
const messages = defineMessages({
'learn.loading.outline': {
id: 'learn.loading.learning.sequence',
defaultMessage: 'Loading learning sequence...',
description: 'Message when learning sequence is being loaded',
},
});
export default messages;
......@@ -4,6 +4,7 @@ import { useSelector, useDispatch } from 'react-redux';
import { history } from '@edx/frontend-platform';
import { getLocale } from '@edx/frontend-platform/i18n';
import { useRouteMatch, Redirect } from 'react-router';
import {
fetchCourse,
fetchSequence,
......@@ -14,9 +15,9 @@ import {
saveSequencePosition,
} from './data/thunks';
import { useModel } from '../model-store';
import { TabPage } from '../tab-page';
import Course from './course';
import { sequenceIdsSelector, firstSequenceIdSelector } from './data/selectors';
function useUnitNavigationHandler(courseId, sequenceId, unitId) {
......@@ -200,7 +201,10 @@ export default function CoursewareContainer() {
}
return (
<main className="flex-grow-1 d-flex flex-column">
<TabPage
activeTabSlug="courseware"
courseId={courseId}
>
<Course
courseId={courseId}
sequenceId={sequenceId}
......@@ -209,7 +213,7 @@ export default function CoursewareContainer() {
previousSequenceHandler={previousSequenceHandler}
unitNavigationHandler={unitNavigationHandler}
/>
</main>
</TabPage>
);
}
......
import React from 'react';
import PropTypes from 'prop-types';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { useSelector } from 'react-redux';
import { AlertList } from '../../user-messages';
import { useAccessExpirationAlert } from '../../access-expiration-alert';
import { useLogistrationAlert } from '../../logistration-alert';
import { useEnrollmentAlert } from '../../enrollment-alert';
import { useOfferAlert } from '../../offer-alert';
import PageLoading from '../../PageLoading';
import InstructorToolbar from './InstructorToolbar';
import Sequence from './sequence';
import CourseBreadcrumbs from './CourseBreadcrumbs';
import { Header, CourseTabsNavigation } from '../../course-header';
import CourseSock from './course-sock';
import ContentTools from './tools/ContentTools';
import messages from './messages';
import { useModel } from '../../model-store';
// Note that we import from the component files themselves in the enrollment-alert package.
......@@ -37,90 +29,53 @@ function Course({
nextSequenceHandler,
previousSequenceHandler,
unitNavigationHandler,
intl,
}) {
const course = useModel('courses', courseId);
const sequence = useModel('sequences', sequenceId);
const section = useModel('sections', sequence ? sequence.sectionId : null);
useOfferAlert(courseId);
useLogistrationAlert();
useEnrollmentAlert(courseId);
useAccessExpirationAlert(courseId);
const courseStatus = useSelector(state => state.courseware.courseStatus);
const {
canShowUpgradeSock,
verifiedMode,
} = course;
if (courseStatus === 'loading') {
return (
<PageLoading
srMessage={intl.formatMessage(messages['learn.loading.learning.sequence'])}
/>
);
}
if (courseStatus === 'loaded') {
const {
canShowUpgradeSock,
org, number, title, isStaff, tabs, verifiedMode,
} = course;
return (
<>
<Header
courseOrg={org}
courseNumber={number}
courseTitle={title}
/>
{isStaff && (
<InstructorToolbar
courseId={courseId}
unitId={unitId}
/>
)}
<CourseTabsNavigation tabs={tabs} activeTabSlug="courseware" />
<div className="container-fluid">
<AlertList
className="my-3"
topic="course"
customAlerts={{
clientEnrollmentAlert: EnrollmentAlert,
clientStaffEnrollmentAlert: StaffEnrollmentAlert,
clientLogistrationAlert: LogistrationAlert,
clientAccessExpirationAlert: AccessExpirationAlert,
clientOfferAlert: OfferAlert,
}}
// courseId is provided because EnrollmentAlert and StaffEnrollmentAlert require it.
customProps={{
courseId,
}}
/>
<CourseBreadcrumbs
courseId={courseId}
sectionId={section ? section.id : null}
sequenceId={sequenceId}
/>
<AlertList topic="sequence" />
</div>
<div className="flex-grow-1 d-flex flex-column">
<Sequence
unitId={unitId}
sequenceId={sequenceId}
courseId={courseId}
unitNavigationHandler={unitNavigationHandler}
nextSequenceHandler={nextSequenceHandler}
previousSequenceHandler={previousSequenceHandler}
/>
{canShowUpgradeSock && verifiedMode && <CourseSock verifiedMode={verifiedMode} />}
<ContentTools course={course} />
</div>
</>
);
}
// courseStatus 'failed' and any other unexpected course status.
return (
<p className="text-center py-5 mx-auto" style={{ maxWidth: '30em' }}>
{intl.formatMessage(messages['learn.course.load.failure'])}
</p>
<>
<AlertList
className="my-3"
topic="course"
customAlerts={{
clientEnrollmentAlert: EnrollmentAlert,
clientStaffEnrollmentAlert: StaffEnrollmentAlert,
clientLogistrationAlert: LogistrationAlert,
clientAccessExpirationAlert: AccessExpirationAlert,
clientOfferAlert: OfferAlert,
}}
// courseId is provided because EnrollmentAlert and StaffEnrollmentAlert require it.
customProps={{
courseId,
}}
/>
<CourseBreadcrumbs
courseId={courseId}
sectionId={section ? section.id : null}
sequenceId={sequenceId}
/>
<AlertList topic="sequence" />
<Sequence
unitId={unitId}
sequenceId={sequenceId}
courseId={courseId}
unitNavigationHandler={unitNavigationHandler}
nextSequenceHandler={nextSequenceHandler}
previousSequenceHandler={previousSequenceHandler}
/>
{canShowUpgradeSock && verifiedMode && <CourseSock verifiedMode={verifiedMode} />}
<ContentTools course={course} />
</>
);
}
......@@ -131,7 +86,6 @@ Course.propTypes = {
nextSequenceHandler: PropTypes.func.isRequired,
previousSequenceHandler: PropTypes.func.isRequired,
unitNavigationHandler: PropTypes.func.isRequired,
intl: intlShape.isRequired,
};
Course.defaultProps = {
......@@ -140,4 +94,4 @@ Course.defaultProps = {
unitId: null,
};
export default injectIntl(Course);
export default Course;
......@@ -3,6 +3,20 @@ import { getConfig, camelCaseObject } from '@edx/frontend-platform';
import { getAuthenticatedHttpClient, getAuthenticatedUser } from '@edx/frontend-platform/auth';
import { logError } from '@edx/frontend-platform/logging';
function overrideTabUrls(id, tabs) {
// "LMS tab slug" to "MFE URL slug" for overridden tabs
const tabOverrides = {};
return tabs.map((tab) => {
let url;
if (tabOverrides[tab.slug]) {
url = `/course/${id}/${tabOverrides[tab.slug]}`;
} else {
url = `${getConfig().LMS_BASE_URL}${tab.url}`;
}
return { ...tab, url };
});
}
function normalizeMetadata(metadata) {
return {
canShowUpgradeSock: metadata.can_show_upgrade_sock,
......@@ -23,7 +37,7 @@ function normalizeMetadata(metadata) {
canLoadCourseware: camelCaseObject(metadata.can_load_courseware),
isStaff: metadata.is_staff,
verifiedMode: camelCaseObject(metadata.verified_mode),
tabs: camelCaseObject(metadata.tabs),
tabs: overrideTabUrls(metadata.id, camelCaseObject(metadata.tabs)),
showCalculator: metadata.show_calculator,
notes: camelCaseObject(metadata.notes),
};
......
......@@ -18,9 +18,10 @@ import { UserMessagesProvider } from './user-messages';
import './index.scss';
import './assets/favicon.ico';
import CourseHome from './course-home';
import CoursewareContainer from './courseware';
import CourseHomeContainer from './course-home';
import CoursewareRedirect from './CoursewareRedirect';
import { TabContainer } from './tab-page';
import store from './store';
......@@ -30,7 +31,11 @@ subscribe(APP_READY, () => {
<UserMessagesProvider>
<Switch>
<Route path="/redirect" component={CoursewareRedirect} />
<Route path="/course/:courseId/home" component={CourseHomeContainer} />
<Route path="/course/:courseId/home">
<TabContainer tab="courseware">
<CourseHome />
</TabContainer>
</Route>
<Route
path={[
'/course/:courseId/:sequenceId/:unitId',
......
......@@ -108,7 +108,6 @@ $primary: #1176B2;
@media (min-width: map-get($grid-breakpoints, 'sm')) {
max-width: 1440px;
width: 100%;
padding: 0 $grid-gutter-width;
margin-right: auto;
margin-left: auto;
}
......
import React from 'react';
import PropTypes from 'prop-types';
import { Header, CourseTabsNavigation } from '../course-header';
import { useModel } from '../model-store';
import { useEnrollmentAlert } from '../enrollment-alert';
import InstructorToolbar from '../courseware/course/InstructorToolbar';
function LoadedTabPage({
activeTabSlug,
children,
courseId,
unitId,
}) {
useEnrollmentAlert(courseId);
const {
isStaff,
number,
org,
tabs,
title,
} = useModel('courses', courseId);
return (
<>
<Header
courseOrg={org}
courseNumber={number}
courseTitle={title}
/>
{isStaff && (
<InstructorToolbar
courseId={courseId}
unitId={unitId}
/>
)}
<main className="d-flex flex-column flex-grow-1">
<CourseTabsNavigation tabs={tabs} className="mb-3" activeTabSlug={activeTabSlug} />
<div className="container-fluid">
{children}
</div>
</main>
</>
);
}
LoadedTabPage.propTypes = {
activeTabSlug: PropTypes.string.isRequired,
children: PropTypes.node,
courseId: PropTypes.string.isRequired,
unitId: PropTypes.string,
};
LoadedTabPage.defaultProps = {
children: null,
unitId: null,
};
export default LoadedTabPage;
import React, { useEffect } from 'react';
import PropTypes from 'prop-types';
import { useDispatch, useSelector } from 'react-redux';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { useParams } from 'react-router-dom';
import messages from './messages';
import PageLoading from '../PageLoading';
import CourseHome from './CourseHome';
import { fetchCourse } from '../data';
function CourseHomeContainer(props) {
import TabPage from './TabPage';
export default function TabContainer(props) {
const {
intl,
match,
children,
tab,
} = props;
const { courseId: courseIdFromUrl } = useParams();
const dispatch = useDispatch();
useEffect(() => {
// The courseId from the URL is the course we WANT to load.
dispatch(fetchCourse(match.params.courseId));
}, [match.params.courseId]);
dispatch(fetchCourse(courseIdFromUrl));
}, [courseIdFromUrl]);
// The courseId from the store is the course we HAVE loaded. If the URL changes,
// we don't want the application to adjust to it until it has actually loaded the new data.
const {
courseId,
courseStatus,
} = useSelector(state => state.courseware);
return (
<>
{courseStatus === 'loaded' ? (
<CourseHome
courseId={courseId}
/>
) : (
<PageLoading
srMessage={intl.formatMessage(messages['learn.loading.outline'])}
/>
)}
</>
<TabPage
activeTabSlug={tab}
courseId={courseId}
>
{children}
</TabPage>
);
}
CourseHomeContainer.propTypes = {
intl: intlShape.isRequired,
match: PropTypes.shape({
params: PropTypes.shape({
courseId: PropTypes.string.isRequired,
}).isRequired,
}).isRequired,