-
Notifications
You must be signed in to change notification settings - Fork 81
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
53 changed files
with
2,124 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
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
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,127 @@ | ||
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 { | ||
Container, Layout, Button, Card, | ||
} from '@edx/paragon'; | ||
import { ArrowCircleDown as ArrowCircleDownIcon } from '@edx/paragon/icons'; | ||
import Cookies from 'universal-cookie'; | ||
import { getConfig } from '@edx/frontend-platform'; | ||
import { Helmet } from 'react-helmet'; | ||
|
||
import InternetConnectionAlert from '../generic/internet-connection-alert'; | ||
import SubHeader from '../generic/sub-header/SubHeader'; | ||
import { RequestStatus } from '../data/constants'; | ||
import { useModel } from '../generic/model-store'; | ||
import messages from './messages'; | ||
import ExportSidebar from './export-sidebar/ExportSidebar'; | ||
import { | ||
getCurrentStage, getError, getExportTriggered, getLoadingStatus, getSavingStatus, | ||
} from './data/selectors'; | ||
import { startExportingCourse } from './data/thunks'; | ||
import { EXPORT_STAGES, LAST_EXPORT_COOKIE_NAME } from './data/constants'; | ||
import { updateExportTriggered, updateSavingStatus, updateSuccessDate } from './data/slice'; | ||
import ExportModalError from './export-modal-error/ExportModalError'; | ||
import ExportFooter from './export-footer/ExportFooter'; | ||
import ExportStepper from './export-stepper/ExportStepper'; | ||
|
||
const CourseExportPage = ({ intl, courseId }) => { | ||
const dispatch = useDispatch(); | ||
const exportTriggered = useSelector(getExportTriggered); | ||
const courseDetails = useModel('courseDetails', courseId); | ||
const currentStage = useSelector(getCurrentStage); | ||
const { msg: errorMessage } = useSelector(getError); | ||
const loadingStatus = useSelector(getLoadingStatus); | ||
const savingStatus = useSelector(getSavingStatus); | ||
const cookies = new Cookies(); | ||
const isShowExportButton = !exportTriggered || errorMessage || currentStage === EXPORT_STAGES.SUCCESS; | ||
const anyRequestFailed = savingStatus === RequestStatus.FAILED || loadingStatus === RequestStatus.FAILED; | ||
const anyRequestInProgress = savingStatus === RequestStatus.PENDING || loadingStatus === RequestStatus.IN_PROGRESS; | ||
|
||
useEffect(() => { | ||
const cookieData = cookies.get(LAST_EXPORT_COOKIE_NAME); | ||
if (cookieData) { | ||
dispatch(updateSavingStatus({ status: RequestStatus.SUCCESSFUL })); | ||
dispatch(updateExportTriggered(true)); | ||
dispatch(updateSuccessDate(cookieData.date)); | ||
} | ||
}, []); | ||
|
||
return ( | ||
<> | ||
<Helmet> | ||
<title> | ||
{intl.formatMessage(messages.pageTitle, { | ||
headingTitle: intl.formatMessage(messages.headingTitle), | ||
courseName: courseDetails?.name, | ||
siteName: process.env.SITE_NAME, | ||
})} | ||
</title> | ||
</Helmet> | ||
<Container size="xl" className="m-4 export"> | ||
<section className="setting-items mb-4"> | ||
<Layout | ||
lg={[{ span: 9 }, { span: 3 }]} | ||
md={[{ span: 9 }, { span: 3 }]} | ||
sm={[{ span: 9 }, { span: 3 }]} | ||
xs={[{ span: 9 }, { span: 3 }]} | ||
xl={[{ span: 9 }, { span: 3 }]} | ||
> | ||
<Layout.Element> | ||
<article> | ||
<SubHeader | ||
title={intl.formatMessage(messages.headingTitle)} | ||
subtitle={intl.formatMessage(messages.headingSubtitle)} | ||
/> | ||
<p>{intl.formatMessage(messages.description1, { studioShortName: getConfig().STUDIO_SHORT_NAME })}</p> | ||
<p>{intl.formatMessage(messages.description2)}</p> | ||
<Card> | ||
<Card.Header | ||
className="h3 px-3 text-black mb-4" | ||
title={intl.formatMessage(messages.titleUnderButton)} | ||
/> | ||
{isShowExportButton && ( | ||
<Card.Section className="px-3 py-1"> | ||
<Button | ||
size="lg" | ||
block | ||
className="mb-4" | ||
onClick={() => dispatch(startExportingCourse(courseId))} | ||
iconBefore={ArrowCircleDownIcon} | ||
> | ||
{intl.formatMessage(messages.buttonTitle)} | ||
</Button> | ||
</Card.Section> | ||
)} | ||
</Card> | ||
{exportTriggered && <ExportStepper courseId={courseId} />} | ||
<ExportFooter /> | ||
</article> | ||
</Layout.Element> | ||
<Layout.Element> | ||
<ExportSidebar courseId={courseId} /> | ||
</Layout.Element> | ||
</Layout> | ||
</section> | ||
<ExportModalError courseId={courseId} /> | ||
</Container> | ||
<div className="alert-toast"> | ||
<InternetConnectionAlert | ||
isFailed={anyRequestFailed} | ||
isQueryPending={anyRequestInProgress} | ||
onInternetConnectionFailed={() => null} | ||
/> | ||
</div> | ||
</> | ||
); | ||
}; | ||
|
||
CourseExportPage.propTypes = { | ||
intl: intlShape.isRequired, | ||
courseId: PropTypes.string.isRequired, | ||
}; | ||
|
||
CourseExportPage.defaultProps = {}; | ||
|
||
export default injectIntl(CourseExportPage); |
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,9 @@ | ||
@import "./export-stepper/ExportStepper"; | ||
@import "./export-footer/ExportFooter"; | ||
@import "./export-sidebar/ExportSidebar"; | ||
|
||
.export { | ||
.help-sidebar { | ||
margin-top: 7.188rem; | ||
} | ||
} |
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,79 @@ | ||
import React from 'react'; | ||
import { initializeMockApp } from '@edx/frontend-platform'; | ||
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth'; | ||
import { IntlProvider, injectIntl } from '@edx/frontend-platform/i18n'; | ||
import { AppProvider } from '@edx/frontend-platform/react'; | ||
import { fireEvent, render, waitFor } from '@testing-library/react'; | ||
import MockAdapter from 'axios-mock-adapter'; | ||
import { Helmet } from 'react-helmet'; | ||
|
||
import initializeStore from '../store'; | ||
import { exportPageMock } from './__mocks__'; | ||
import messages from './messages'; | ||
import CourseExportPage from './CourseExportPage'; | ||
import { postExportCourseApiUrl } from './data/api'; | ||
|
||
let store; | ||
let axiosMock; | ||
const courseId = '123'; | ||
const courseName = 'About Node JS'; | ||
|
||
jest.mock('../generic/model-store', () => ({ | ||
useModel: jest.fn().mockReturnValue({ | ||
name: courseName, | ||
}), | ||
})); | ||
|
||
const RootWrapper = () => ( | ||
<AppProvider store={store}> | ||
<IntlProvider locale="en" messages={{}}> | ||
<CourseExportPage intl={injectIntl} courseId={courseId} /> | ||
</IntlProvider> | ||
</AppProvider> | ||
); | ||
|
||
describe('<CourseExportPage />', () => { | ||
beforeEach(() => { | ||
initializeMockApp({ | ||
authenticatedUser: { | ||
userId: 3, | ||
username: 'abc123', | ||
administrator: true, | ||
roles: [], | ||
}, | ||
}); | ||
store = initializeStore(); | ||
axiosMock = new MockAdapter(getAuthenticatedHttpClient()); | ||
axiosMock | ||
.onGet(postExportCourseApiUrl(courseId)) | ||
.reply(200, exportPageMock); | ||
}); | ||
it('should render page title correctly', async () => { | ||
render(<RootWrapper />); | ||
await waitFor(() => { | ||
const helmet = Helmet.peek(); | ||
expect(helmet.title).toEqual( | ||
`${messages.headingTitle.defaultMessage} | ${courseName} | ${process.env.SITE_NAME}`, | ||
); | ||
}); | ||
}); | ||
it('should render without errors', async () => { | ||
const { getByText } = render(<RootWrapper />); | ||
await waitFor(() => { | ||
expect(getByText(messages.headingSubtitle.defaultMessage)).toBeInTheDocument(); | ||
const exportPageElement = getByText(messages.headingTitle.defaultMessage, { | ||
selector: 'h2.sub-header-title', | ||
}); | ||
expect(exportPageElement).toBeInTheDocument(); | ||
expect(getByText(messages.titleUnderButton.defaultMessage)).toBeInTheDocument(); | ||
expect(getByText(messages.description2.defaultMessage)).toBeInTheDocument(); | ||
expect(getByText(messages.buttonTitle.defaultMessage)).toBeInTheDocument(); | ||
}); | ||
}); | ||
it('should start exporting on click', async () => { | ||
const { getByText, container } = render(<RootWrapper />); | ||
const button = container.querySelector('.btn-primary'); | ||
fireEvent.click(button); | ||
expect(getByText(/Preparing to start the export/i)).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,3 @@ | ||
module.exports = { | ||
exportStatus: 1, | ||
}; |
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 @@ | ||
// eslint-disable-next-line import/prefer-default-export | ||
export { default as exportPageMock } from './exportPage'; |
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,19 @@ | ||
/* eslint-disable import/prefer-default-export */ | ||
import { camelCaseObject, getConfig } from '@edx/frontend-platform'; | ||
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth'; | ||
|
||
const getApiBaseUrl = () => getConfig().STUDIO_BASE_URL; | ||
export const postExportCourseApiUrl = (courseId) => new URL(`export/${courseId}`, getApiBaseUrl()).href; | ||
export const getExportStatusApiUrl = (courseId) => new URL(`export_status/${courseId}`, getApiBaseUrl()).href; | ||
|
||
export async function startCourseExporting(courseId) { | ||
const { data } = await getAuthenticatedHttpClient() | ||
.post(postExportCourseApiUrl(courseId)); | ||
return camelCaseObject(data); | ||
} | ||
|
||
export async function getExportStatus(courseId) { | ||
const { data } = await getAuthenticatedHttpClient() | ||
.get(getExportStatusApiUrl(courseId)); | ||
return camelCaseObject(data); | ||
} |
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,8 @@ | ||
export const LAST_EXPORT_COOKIE_NAME = 'lastexport'; | ||
export const EXPORT_STAGES = { | ||
PREPARING: 0, | ||
EXPORTING: 1, | ||
COMPRESSING: 2, | ||
SUCCESS: 3, | ||
}; | ||
export const SUCCESS_DATE_FORMAT = 'MM/DD/yyyy'; |
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,8 @@ | ||
export const getExportTriggered = (state) => state.courseExport.exportTriggered; | ||
export const getCurrentStage = (state) => state.courseExport.currentStage; | ||
export const getDownloadPath = (state) => state.courseExport.downloadPath; | ||
export const getSuccessDate = (state) => state.courseExport.successDate; | ||
export const getError = (state) => state.courseExport.error; | ||
export const getIsErrorModalOpen = (state) => state.courseExport.isErrorModalOpen; | ||
export const getLoadingStatus = (state) => state.courseExport.loadingStatus; | ||
export const getSavingStatus = (state) => state.courseExport.savingStatus; |
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,63 @@ | ||
/* eslint-disable no-param-reassign */ | ||
import { createSlice } from '@reduxjs/toolkit'; | ||
|
||
const initialState = { | ||
exportTriggered: false, | ||
currentStage: 0, | ||
error: { msg: null, unitUrl: null }, | ||
downloadPath: null, | ||
successDate: null, | ||
isErrorModalOpen: false, | ||
loadingStatus: '', | ||
savingStatus: '', | ||
}; | ||
|
||
const slice = createSlice({ | ||
name: 'exportPage', | ||
initialState, | ||
reducers: { | ||
updateExportTriggered: (state, { payload }) => { | ||
state.exportTriggered = payload; | ||
}, | ||
updateCurrentStage: (state, { payload }) => { | ||
if (payload >= state.currentStage) { | ||
state.currentStage = payload; | ||
} | ||
}, | ||
updateDownloadPath: (state, { payload }) => { | ||
state.downloadPath = payload; | ||
}, | ||
updateSuccessDate: (state, { payload }) => { | ||
state.successDate = payload; | ||
}, | ||
updateError: (state, { payload }) => { | ||
state.error = payload; | ||
}, | ||
updateIsErrorModalOpen: (state, { payload }) => { | ||
state.isErrorModalOpen = payload; | ||
}, | ||
reset: () => initialState, | ||
updateLoadingStatus: (state, { payload }) => { | ||
state.loadingStatus = payload.status; | ||
}, | ||
updateSavingStatus: (state, { payload }) => { | ||
state.savingStatus = payload.status; | ||
}, | ||
}, | ||
}); | ||
|
||
export const { | ||
updateExportTriggered, | ||
updateCurrentStage, | ||
updateDownloadPath, | ||
updateSuccessDate, | ||
updateError, | ||
updateIsErrorModalOpen, | ||
reset, | ||
updateLoadingStatus, | ||
updateSavingStatus, | ||
} = slice.actions; | ||
|
||
export const { | ||
reducer, | ||
} = slice; |
Oops, something went wrong.