forked from openedx/frontend-app-authoring
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: course outline page (openedx#694)
* 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
1 parent
bebbc15
commit 04c1427
Showing
61 changed files
with
5,507 additions
and
21 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
@import "./header-navigations/HeaderNavigations"; | ||
@import "./status-bar/StatusBar"; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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, | ||
}, | ||
}, | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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, | ||
}, | ||
}; |
Oops, something went wrong.