Skip to content

Commit

Permalink
feat: refactor non instruction components
Browse files Browse the repository at this point in the history
  • Loading branch information
alangsto committed Feb 2, 2024
1 parent b5fce7d commit bc11f34
Show file tree
Hide file tree
Showing 13 changed files with 186 additions and 215 deletions.
8 changes: 4 additions & 4 deletions src/context.jsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React from 'react';

const ExamStateContext = React.createContext({});
export default ExamStateContext;
// import React from 'react';
//
// const ExamStateContext = React.createContext({});
// export default ExamStateContext;
70 changes: 35 additions & 35 deletions src/core/ExamStateProvider.jsx
Original file line number Diff line number Diff line change
@@ -1,35 +1,35 @@
import React, { useMemo } from 'react';
import { withExamStore } from '../hocs';
import * as dispatchActions from '../data/thunks';
import ExamStateContext from '../context';
import { IS_STARTED_STATUS } from '../constants';

/**
* Make exam state available as a context for all library components.
* @param children - sequence content
* @param state - exam state params and actions
* @returns {JSX.Element}
*/

// eslint-disable-next-line react/prop-types
const StateProvider = ({ children, ...state }) => {
const contextValue = useMemo(() => ({
...state,
showTimer: !!(state.activeAttempt && IS_STARTED_STATUS(state.activeAttempt.attempt_status)),
}), [state]);
return (
<ExamStateContext.Provider value={contextValue}>
{children}
</ExamStateContext.Provider>
);
};

const mapStateToProps = (state) => ({ ...state.specialExams });

const ExamStateProvider = withExamStore(
StateProvider,
mapStateToProps,
dispatchActions,
);

export default ExamStateProvider;
// import React, { useMemo } from 'react';
// import { withExamStore } from '../hocs';
// import * as dispatchActions from '../data/thunks';
// import ExamStateContext from '../context';
// import { IS_STARTED_STATUS } from '../constants';
//
// /**
// * Make exam state available as a context for all library components.
// * @param children - sequence content
// * @param state - exam state params and actions
// * @returns {JSX.Element}
// */
//
// // eslint-disable-next-line react/prop-types
// const StateProvider = ({ children, ...state }) => {
// const contextValue = useMemo(() => ({
// ...state,
// showTimer: !!(state.activeAttempt && IS_STARTED_STATUS(state.activeAttempt.attempt_status)),
// }), [state]);
// return (
// <ExamStateContext.Provider value={contextValue}>
// {children}
// </ExamStateContext.Provider>
// );
// };
//
// const mapStateToProps = (state) => ({ ...state.specialExams });
//
// const ExamStateProvider = withExamStore(
// StateProvider,
// mapStateToProps,
// dispatchActions,
// );
//
// export default ExamStateProvider;
41 changes: 24 additions & 17 deletions src/core/OuterExamTimer.jsx
Original file line number Diff line number Diff line change
@@ -1,23 +1,32 @@
import React, { useEffect, useContext } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import PropTypes from 'prop-types';
import { AppContext } from '@edx/frontend-platform/react';
import ExamStateContext from '../context';
import { ExamTimerBlock } from '../timer';
import ExamAPIError from '../exam/ExamAPIError';
import ExamStateProvider from './ExamStateProvider';
import {
getLatestAttemptData,
stopExam,
submitExam,
expireExam,
pollAttempt,
pingAttempt,
} from '../data/thunks';
import { IS_STARTED_STATUS } from '../constants';

const ExamTimer = ({ courseId }) => {
const state = useContext(ExamStateContext);
const { authenticatedUser } = useContext(AppContext);

const {
activeAttempt, showTimer, stopExam, submitExam,
expireExam, pollAttempt, apiErrorMsg, pingAttempt,
getLatestAttemptData,
} = state;
activeAttempt, apiErrorMsg,
} = useSelector(state => state.specialExams);
const dispatch = useDispatch();

const showTimer = !!(activeAttempt && IS_STARTED_STATUS(activeAttempt.attempt_status));

useEffect(() => {
getLatestAttemptData(courseId);
// eslint-disable-next-line react-hooks/exhaustive-deps
dispatch(getLatestAttemptData(courseId));
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [courseId]);

// if user is not authenticated they cannot have active exam, so no need for timer
Expand All @@ -31,11 +40,11 @@ const ExamTimer = ({ courseId }) => {
{showTimer && (
<ExamTimerBlock
attempt={activeAttempt}
stopExamAttempt={stopExam}
submitExam={submitExam}
expireExamAttempt={expireExam}
pollExamAttempt={pollAttempt}
pingAttempt={pingAttempt}
stopExamAttempt={() => dispatch(stopExam())}
submitExam={() => dispatch(submitExam())}
expireExamAttempt={() => dispatch(expireExam())}
pollExamAttempt={(url) => dispatch(pollAttempt(url))}
pingAttempt={(timeout, url) => dispatch(pingAttempt(timeout, url))}
/>
)}
{apiErrorMsg && <ExamAPIError />}
Expand All @@ -53,9 +62,7 @@ ExamTimer.propTypes = {
* will be shown.
*/
const OuterExamTimer = ({ courseId }) => (
<ExamStateProvider>
<ExamTimer courseId={courseId} />
</ExamStateProvider>
<ExamTimer courseId={courseId} />
);

OuterExamTimer.propTypes = {
Expand Down
5 changes: 1 addition & 4 deletions src/core/SequenceExamWrapper.jsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import React from 'react';
import ExamWrapper from '../exam/ExamWrapper';
import ExamStateProvider from './ExamStateProvider';

/**
* SequenceExamWrapper is the component responsible for handling special exams.
Expand All @@ -14,9 +13,7 @@ import ExamStateProvider from './ExamStateProvider';
* </SequenceExamWrapper>
*/
const SequenceExamWrapper = (props) => (
<ExamStateProvider>
<ExamWrapper {...props} />
</ExamStateProvider>
<ExamWrapper {...props} />
);

export default SequenceExamWrapper;
16 changes: 8 additions & 8 deletions src/data/store.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { configureStore } from '@reduxjs/toolkit';
import examReducer from './slice';

export default configureStore({
reducer: {
specialExams: examReducer,
},
});
// import { configureStore } from '@reduxjs/toolkit';
// import examReducer from './slice';
//
// export default configureStore({
// reducer: {
// examState: examReducer,
// },
// });
38 changes: 23 additions & 15 deletions src/exam/Exam.jsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,22 @@
/* eslint-disable react-hooks/exhaustive-deps */
import React, { useContext, useEffect, useState } from 'react';
import React, { useEffect, useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import PropTypes from 'prop-types';
import { injectIntl, intlShape, FormattedMessage } from '@edx/frontend-platform/i18n';
import { Alert, Spinner } from '@edx/paragon';
import { Info } from '@edx/paragon/icons';
import { ExamTimerBlock } from '../timer';
import Instructions from '../instructions';
import ExamStateContext from '../context';
import ExamAPIError from './ExamAPIError';
import { ExamStatus, ExamType } from '../constants';
import { ExamStatus, ExamType, IS_STARTED_STATUS } from '../constants';
import messages from './messages';
import {
getProctoringSettings,
stopExam,
submitExam,
expireExam,
pollAttempt,
pingAttempt,
} from '../data/thunks';

/**
* Exam component is intended to render exam instructions before and after exam.
Expand All @@ -23,12 +30,12 @@ import messages from './messages';
const Exam = ({
isGated, isTimeLimited, originalUserIsStaff, canAccessProctoredExams, children, intl,
}) => {
const state = useContext(ExamStateContext);
const {
isLoading, activeAttempt, showTimer, stopExam, exam,
expireExam, pollAttempt, apiErrorMsg, pingAttempt,
getProctoringSettings, submitExam,
} = state;
isLoading, activeAttempt, exam, apiErrorMsg,
} = useSelector(state => state.specialExams);
const dispatch = useDispatch();

const showTimer = !!(activeAttempt && IS_STARTED_STATUS(activeAttempt.attempt_status));

const {
attempt,
Expand Down Expand Up @@ -61,7 +68,7 @@ const Exam = ({
if (proctoredExamTypes.includes(examType)) {
// only fetch proctoring settings for a proctored exam
if (examId) {
getProctoringSettings();
dispatch(getProctoringSettings());
}

// Only exclude Timed Exam when restricting access to exams
Expand All @@ -70,6 +77,7 @@ const Exam = ({
// this makes sure useEffect gets called only one time after the exam has been fetched
// we can't leave this empty since initially exam is just an empty object, so
// API calls above would not get triggered
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [examId]);

if (isLoading) {
Expand Down Expand Up @@ -106,11 +114,11 @@ const Exam = ({
{showTimer && (
<ExamTimerBlock
attempt={activeAttempt}
stopExamAttempt={stopExam}
submitExam={submitExam}
expireExamAttempt={expireExam}
pollExamAttempt={pollAttempt}
pingAttempt={pingAttempt}
stopExamAttempt={() => dispatch(stopExam())}
submitExam={() => dispatch(submitExam())}
expireExamAttempt={() => dispatch(expireExam())}
pollExamAttempt={(url) => dispatch(pollAttempt(url))}
pingAttempt={(timeout, url) => dispatch(pingAttempt(timeout, url))}
/>
)}
{ // show the error message only if you are in the exam sequence
Expand Down
7 changes: 3 additions & 4 deletions src/exam/ExamAPIError.jsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,14 @@
import React, { useContext } from 'react';
import React from 'react';
import { useSelector } from 'react-redux';
import { getConfig } from '@edx/frontend-platform';
import { Alert, Hyperlink, Icon } from '@edx/paragon';
import { Info } from '@edx/paragon/icons';
import { injectIntl, intlShape, FormattedMessage } from '@edx/frontend-platform/i18n';
import ExamStateContext from '../context';
import messages from './messages';

const ExamAPIError = ({ intl }) => {
const state = useContext(ExamStateContext);
const { SITE_NAME, SUPPORT_URL } = getConfig();
const { apiErrorMsg } = state;
const { apiErrorMsg } = useSelector(state => state.specialExams);
const shouldShowApiErrorMsg = !!apiErrorMsg && !apiErrorMsg.includes('<');

return (
Expand Down
21 changes: 5 additions & 16 deletions src/exam/ExamAPIError.test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import React from 'react';
import { getConfig } from '@edx/frontend-platform';
import { store } from '../data';
import { render } from '../setupTest';
import ExamStateProvider from '../core/ExamStateProvider';
import ExamAPIError from './ExamAPIError';

const originalConfig = jest.requireActual('@edx/frontend-platform').getConfig();
Expand All @@ -26,9 +25,7 @@ describe('ExamAPIError', () => {
store.getState = () => ({ specialExams: {} });

const tree = render(
<ExamStateProvider>
<ExamAPIError />
</ExamStateProvider>,
<ExamAPIError />,
{ store },
);

Expand All @@ -45,9 +42,7 @@ describe('ExamAPIError', () => {
store.getState = () => ({ specialExams: {} });

const { getByTestId } = render(
<ExamStateProvider>
<ExamAPIError />
</ExamStateProvider>,
<ExamAPIError />,
{ store },
);

Expand All @@ -62,9 +57,7 @@ describe('ExamAPIError', () => {
});

const { queryByTestId } = render(
<ExamStateProvider>
<ExamAPIError />
</ExamStateProvider>,
<ExamAPIError />,
{ store },
);

Expand All @@ -77,9 +70,7 @@ describe('ExamAPIError', () => {
});

const { queryByTestId } = render(
<ExamStateProvider>
<ExamAPIError />
</ExamStateProvider>,
<ExamAPIError />,
{ store },
);

Expand All @@ -92,9 +83,7 @@ describe('ExamAPIError', () => {
});

const { queryByTestId } = render(
<ExamStateProvider>
<ExamAPIError />
</ExamStateProvider>,
<ExamAPIError />,
{ store },
);

Expand Down
20 changes: 14 additions & 6 deletions src/exam/ExamWrapper.jsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,18 @@
import { useDispatch, useSelector } from 'react-redux';
import React, { useContext, useEffect } from 'react';
import { AppContext } from '@edx/frontend-platform/react';
import PropTypes from 'prop-types';
import Exam from './Exam';
import ExamStateContext from '../context';
import {
getExamAttemptsData,
getAllowProctoringOptOut,
checkExamEntry,
} from '../data/thunks';

/**
* Exam wrapper is responsible for triggering initial exam data fetching and rendering Exam.
*/
const ExamWrapper = ({ children, ...props }) => {
const state = useContext(ExamStateContext);
const { authenticatedUser } = useContext(AppContext);
const {
sequence,
Expand All @@ -17,9 +21,13 @@ const ExamWrapper = ({ children, ...props }) => {
originalUserIsStaff,
canAccessProctoredExams,
} = props;
const { getExamAttemptsData, getAllowProctoringOptOut, checkExamEntry } = state;

const { isLoading } = useSelector(state => state.specialExams);

const dispatch = useDispatch();

const loadInitialData = async () => {
await getExamAttemptsData(courseId, sequence.id);
await dispatch(getExamAttemptsData(courseId, sequence.id));
await getAllowProctoringOptOut(sequence.allowProctoringOptOut);
await checkExamEntry();
};
Expand All @@ -28,10 +36,10 @@ const ExamWrapper = ({ children, ...props }) => {

useEffect(() => {
// fetch exam data on exam sequences or if no exam data has been fetched yet
if (sequence.isTimeLimited || state.isLoading) {
if (sequence.isTimeLimited || isLoading) {
loadInitialData();
}
// eslint-disable-next-line react-hooks/exhaustive-deps
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);

// if the user is browsing public content (not logged in) they cannot be in an exam
Expand Down
Loading

0 comments on commit bc11f34

Please sign in to comment.