Skip to content

Commit

Permalink
feat: course outline page (openedx#694)
Browse files Browse the repository at this point in the history
* feat: Course outline Top level page (#36)

* feat: [2u-259] add components

* feat: [2u-259] fix sidebar

* feat: [2u-259] add tests, fix links

* feat: [2u-259] fix messages

* feat: [2u-159] fix reducer and sidebar

* feat: [2u-259] fix reducer

* feat: [2u-259] remove warning from selectors

* feat: [2u-259] remove indents

---------

Co-authored-by: Vladislav Keblysh <[email protected]>

feat: Course outline Status Bar (#50)

* feat: [2u-259] add components

* feat: [2u-259] fix sidebar

* feat: [2u-259] add tests, fix links

* feat: [2u-259] fix messages

* feat: [2u-159] fix reducer and sidebar

* feat: add checklist

* feat: [2u-259] fix reducer

* feat: [2u-259] remove warning from selectors

* feat: [2u-259] remove indents

* feat: [2u-259] add api, enable modal

* feat: [2u-259] add tests

* feat: [2u-259] add translates

* feat: [2u-271] fix transalates

* feat: [2u-281] fix isQuery pending, utils, hooks

* feat: [2u-281] fix useScrollToHashElement

* feat: [2u-271] fix imports

---------

Co-authored-by: Vladislav Keblysh <[email protected]>

feat: Course Outline Reindex (#55)

* feat: [2u-277] add alerts

* feat: [2u-277] add translates

* feat: [2u-277] fix tests

* fix: [2u-277] fix slice and hook

---------

Co-authored-by: Vladislav Keblysh <[email protected]>

fix: Course outline tests (#56)

* fix: fixed course outline status bar tests

* fix: fixed course outline status bar tests

* fix: fixed course outline enable highlights modal tests

* fix: enable modal tests

fix: increase code coverage on the page

* refactor: improve course outline page

feat: lms live link

chore: update outline link

fix: course outline link

refactor: remove unnecessary css and rename test file

refactor: remove unnecessary css from outlineSidebar

test: make use of message variable instead of hardcoded text

refactor: remove unnecessary h5 class

test: use test id for detecting component

refactor: update course outline url and some default messages

---------

Co-authored-by: vladislavkeblysh <[email protected]>
  • Loading branch information
navinkarkera and vladislavkeblysh authored Dec 6, 2023
1 parent bebbc15 commit 04c1427
Show file tree
Hide file tree
Showing 61 changed files with 5,507 additions and 21 deletions.
1 change: 0 additions & 1 deletion .env.development
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,6 @@ ENABLE_ACCESSIBILITY_PAGE=false
ENABLE_PROGRESS_GRAPH_SETTINGS=false
ENABLE_TEAM_TYPE_SETTING=false
ENABLE_NEW_EDITOR_PAGES=true
ENABLE_NEW_COURSE_OUTLINE_PAGE = false
ENABLE_NEW_VIDEO_UPLOAD_PAGE = false
ENABLE_UNIT_PAGE = false
ENABLE_VIDEO_UPLOAD_PAGE_LINK_IN_CONTENT_DROPDOWN = false
Expand Down
5 changes: 3 additions & 2 deletions src/CourseAuthoringRoutes.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import VideoSelectorContainer from './selectors/VideoSelectorContainer';
import CustomPages from './custom-pages';
import { FilesPage, VideosPage } from './files-and-videos';
import { AdvancedSettings } from './advanced-settings';
import { CourseOutline } from './course-outline';
import ScheduleAndDetails from './schedule-and-details';
import { GradingSettings } from './grading-settings';
import CourseTeam from './course-team/CourseTeam';
Expand Down Expand Up @@ -41,8 +42,8 @@ const CourseAuthoringRoutes = () => {
<CourseAuthoringPage courseId={courseId}>
<Routes>
<Route
path="outline"
element={process.env.ENABLE_NEW_COURSE_OUTLINE_PAGE === 'true' ? <PageWrap><Placeholder /></PageWrap> : null}
path="/"
element={<PageWrap><CourseOutline courseId={courseId} /></PageWrap>}
/>
<Route
path="course_info"
Expand Down
141 changes: 141 additions & 0 deletions src/course-outline/CourseOutline.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
import React from 'react';
import PropTypes from 'prop-types';
import { useIntl } from '@edx/frontend-platform/i18n';
import {
Container,
Layout,
TransitionReplace,
} from '@edx/paragon';
import {
CheckCircle as CheckCircleIcon,
Warning as WarningIcon,
} from '@edx/paragon/icons';

import SubHeader from '../generic/sub-header/SubHeader';
import { RequestStatus } from '../data/constants';
import InternetConnectionAlert from '../generic/internet-connection-alert';
import AlertMessage from '../generic/alert-message';
import HeaderNavigations from './header-navigations/HeaderNavigations';
import OutlineSideBar from './outline-sidebar/OutlineSidebar';
import messages from './messages';
import { useCourseOutline } from './hooks';
import StatusBar from './status-bar/StatusBar';
import EnableHighlightsModal from './enable-highlights-modal/EnableHighlightsModal';

const CourseOutline = ({ courseId }) => {
const intl = useIntl();

const {
savingStatus,
statusBarData,
isLoading,
isReIndexShow,
showErrorAlert,
showSuccessAlert,
isSectionsExpanded,
isEnableHighlightsModalOpen,
isInternetConnectionAlertFailed,
isDisabledReindexButton,
headerNavigationsActions,
openEnableHighlightsModal,
closeEnableHighlightsModal,
handleEnableHighlightsSubmit,
handleInternetConnectionFailed,
} = useCourseOutline({ courseId });

if (isLoading) {
// eslint-disable-next-line react/jsx-no-useless-fragment
return <></>;
}

return (
<>
<Container size="xl" className="px-4">
<section className="course-outline-container mb-4 mt-5">
<TransitionReplace>
{showSuccessAlert ? (
<AlertMessage
key={intl.formatMessage(messages.alertSuccessAriaLabelledby)}
show={showSuccessAlert}
variant="success"
icon={CheckCircleIcon}
title={intl.formatMessage(messages.alertSuccessTitle)}
description={intl.formatMessage(messages.alertSuccessDescription)}
aria-hidden="true"
aria-labelledby={intl.formatMessage(messages.alertSuccessAriaLabelledby)}
aria-describedby={intl.formatMessage(messages.alertSuccessAriaDescribedby)}
/>
) : null}
</TransitionReplace>
<SubHeader
className="mt-5"
title={intl.formatMessage(messages.headingTitle)}
subtitle={intl.formatMessage(messages.headingSubtitle)}
headerActions={(
<HeaderNavigations
isReIndexShow={isReIndexShow}
isSectionsExpanded={isSectionsExpanded}
headerNavigationsActions={headerNavigationsActions}
isDisabledReindexButton={isDisabledReindexButton}
/>
)}
/>
<Layout
lg={[{ span: 9 }, { span: 3 }]}
md={[{ span: 9 }, { span: 3 }]}
sm={[{ span: 12 }, { span: 12 }]}
xs={[{ span: 12 }, { span: 12 }]}
xl={[{ span: 9 }, { span: 3 }]}
>
<Layout.Element>
<article>
<div>
<section className="course-outline-section">
<StatusBar
courseId={courseId}
isLoading={isLoading}
statusBarData={statusBarData}
openEnableHighlightsModal={openEnableHighlightsModal}
/>
</section>
</div>
</article>
</Layout.Element>
<Layout.Element>
<OutlineSideBar courseId={courseId} />
</Layout.Element>
</Layout>
<EnableHighlightsModal
isOpen={isEnableHighlightsModalOpen}
close={closeEnableHighlightsModal}
onEnableHighlightsSubmit={handleEnableHighlightsSubmit}
highlightsDocUrl={statusBarData.highlightsDocUrl}
/>
</section>
</Container>
<div className="alert-toast">
<InternetConnectionAlert
isFailed={isInternetConnectionAlertFailed}
isQueryPending={savingStatus === RequestStatus.PENDING}
onInternetConnectionFailed={handleInternetConnectionFailed}
/>
{showErrorAlert && (
<AlertMessage
key={intl.formatMessage(messages.alertErrorTitle)}
show={showErrorAlert}
variant="danger"
icon={WarningIcon}
title={intl.formatMessage(messages.alertErrorTitle)}
aria-hidden="true"
/>
)}
</div>
</>
);
};

CourseOutline.propTypes = {
courseId: PropTypes.string.isRequired,
};

export default CourseOutline;
2 changes: 2 additions & 0 deletions src/course-outline/CourseOutline.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
@import "./header-navigations/HeaderNavigations";
@import "./status-bar/StatusBar";
150 changes: 150 additions & 0 deletions src/course-outline/CourseOutline.test.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
import React from 'react';
import { render, waitFor } from '@testing-library/react';
import { IntlProvider } from '@edx/frontend-platform/i18n';
import { AppProvider } from '@edx/frontend-platform/react';
import { initializeMockApp } from '@edx/frontend-platform';
import MockAdapter from 'axios-mock-adapter';
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';

import {
getCourseBestPracticesApiUrl,
getCourseLaunchApiUrl,
getCourseOutlineIndexApiUrl,
getCourseReindexApiUrl,
getEnableHighlightsEmailsApiUrl,
} from './data/api';
import {
enableCourseHighlightsEmailsQuery,
fetchCourseBestPracticesQuery,
fetchCourseLaunchQuery,
fetchCourseOutlineIndexQuery,
fetchCourseReindexQuery,
} from './data/thunk';
import initializeStore from '../store';
import {
courseOutlineIndexMock,
courseBestPracticesMock,
courseLaunchMock,
} from './__mocks__';
import { executeThunk } from '../utils';
import CourseOutline from './CourseOutline';
import messages from './messages';

let axiosMock;
let store;
const mockPathname = '/foo-bar';
const courseId = '123';

jest.mock('react-router-dom', () => ({
...jest.requireActual('react-router-dom'),
useLocation: () => ({
pathname: mockPathname,
}),
}));

const RootWrapper = () => (
<AppProvider store={store}>
<IntlProvider locale="en">
<CourseOutline courseId={courseId} />
</IntlProvider>
</AppProvider>
);

describe('<CourseOutline />', () => {
beforeEach(async () => {
initializeMockApp({
authenticatedUser: {
userId: 3,
username: 'abc123',
administrator: true,
roles: [],
},
});

store = initializeStore();
axiosMock = new MockAdapter(getAuthenticatedHttpClient());
axiosMock
.onGet(getCourseOutlineIndexApiUrl(courseId))
.reply(200, courseOutlineIndexMock);
await executeThunk(fetchCourseOutlineIndexQuery(courseId), store.dispatch);
});

it('render CourseOutline component correctly', async () => {
const { getByText } = render(<RootWrapper />);

await waitFor(() => {
expect(getByText(messages.headingTitle.defaultMessage)).toBeInTheDocument();
expect(getByText(messages.headingSubtitle.defaultMessage)).toBeInTheDocument();
});
});

it('check reindex and render success alert is correctly', async () => {
const { getByText } = render(<RootWrapper />);

axiosMock
.onGet(getCourseReindexApiUrl(courseOutlineIndexMock.reindexLink))
.reply(200);
await executeThunk(fetchCourseReindexQuery(courseId, courseOutlineIndexMock.reindexLink), store.dispatch);

expect(getByText(messages.alertSuccessDescription.defaultMessage)).toBeInTheDocument();
});

it('render error alert after failed reindex correctly', async () => {
const { getByText } = render(<RootWrapper />);

axiosMock
.onGet(getCourseReindexApiUrl('some link'))
.reply(500);
await executeThunk(fetchCourseReindexQuery(courseId, 'some link'), store.dispatch);

expect(getByText(messages.alertErrorTitle.defaultMessage)).toBeInTheDocument();
});

it('render checklist value correctly', async () => {
const { getByText } = render(<RootWrapper />);

axiosMock
.onGet(getCourseBestPracticesApiUrl({
courseId, excludeGraded: true, all: true,
}))
.reply(200, courseBestPracticesMock);

axiosMock
.onGet(getCourseLaunchApiUrl({
courseId, gradedOnly: true, validateOras: true, all: true,
}))
.reply(200, courseLaunchMock);

await executeThunk(fetchCourseLaunchQuery({
courseId, gradedOnly: true, validateOras: true, all: true,
}), store.dispatch);
await executeThunk(fetchCourseBestPracticesQuery({
courseId, excludeGraded: true, all: true,
}), store.dispatch);

expect(getByText('4/9 completed')).toBeInTheDocument();
});

it('check highlights are enabled after enable highlights query is successful', async () => {
const { findByTestId } = render(<RootWrapper />);

axiosMock
.onGet(getCourseOutlineIndexApiUrl(courseId))
.reply(200, {
...courseOutlineIndexMock,
highlightsEnabledForMessaging: false,
});

axiosMock
.onPost(getEnableHighlightsEmailsApiUrl(courseId), {
publish: 'republish',
metadata: {
highlights_enabled_for_messaging: true,
},
})
.reply(200);

await executeThunk(enableCourseHighlightsEmailsQuery(courseId), store.dispatch);
expect(await findByTestId('highlights-enabled-span')).toBeInTheDocument();
});
});
43 changes: 43 additions & 0 deletions src/course-outline/__mocks__/courseBestPractices.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
module.exports = {
isSelfPaced: false,
sections: {
totalNumber: 6,
totalVisible: 4,
numberWithHighlights: 2,
highlightsActiveForCourse: true,
highlightsEnabled: true,
},
subsections: {
totalVisible: 5,
numWithOneBlockType: 2,
numBlockTypes: {
min: 0,
max: 3,
mean: 1,
median: 1,
mode: 1,
},
},
units: {
totalVisible: 9,
numBlocks: {
min: 1,
max: 2,
mean: 2,
median: 2,
mode: 2,
},
},
videos: {
totalNumber: 7,
numMobileEncoded: 0,
numWithValId: 3,
durations: {
min: null,
max: null,
mean: null,
median: null,
mode: null,
},
},
};
31 changes: 31 additions & 0 deletions src/course-outline/__mocks__/courseLaunch.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
module.exports = {
isSelfPaced: false,
dates: {
hasStartDate: true,
hasEndDate: false,
},
assignments: {
totalNumber: 11,
totalVisible: 7,
assignmentsWithDatesBeforeStart: [],
assignmentsWithDatesAfterEnd: [],
assignmentsWithOraDatesBeforeStart: [],
assignmentsWithOraDatesAfterEnd: [],
},
grades: {
hasGradingPolicy: true,
sumOfWeights: 1,
},
certificates: {
isActivated: false,
hasCertificate: false,
isEnabled: true,
},
updates: {
hasUpdate: true,
},
proctoring: {
needsProctoringEscalationEmail: false,
hasProctoringEscalationEmail: false,
},
};
Loading

0 comments on commit 04c1427

Please sign in to comment.