diff --git a/src/course/data/selectors.js b/src/active-course/data/selectors.js similarity index 100% rename from src/course/data/selectors.js rename to src/active-course/data/selectors.js diff --git a/src/course/data/slice.js b/src/active-course/data/slice.js similarity index 67% rename from src/course/data/slice.js rename to src/active-course/data/slice.js index a5b760d7..64b7cbb1 100644 --- a/src/course/data/slice.js +++ b/src/active-course/data/slice.js @@ -1,33 +1,33 @@ /* eslint-disable no-param-reassign */ import { createSlice } from '@reduxjs/toolkit'; -export const LOADING = 'loading'; -export const LOADED = 'loaded'; -export const FAILED = 'failed'; -export const DENIED = 'denied'; +export const COURSE_LOADING = 'loading'; +export const COURSE_LOADED = 'loaded'; +export const COURSE_FAILED = 'failed'; +export const COURSE_DENIED = 'denied'; const slice = createSlice({ name: 'activeCourse', initialState: { - courseStatus: LOADING, + courseStatus: COURSE_LOADING, courseId: null, }, reducers: { fetchCourseRequest: (state, { payload }) => { state.courseId = payload.courseId; - state.courseStatus = LOADING; + state.courseStatus = COURSE_LOADING; }, fetchCourseSuccess: (state, { payload }) => { state.courseId = payload.courseId; - state.courseStatus = LOADED; + state.courseStatus = COURSE_LOADED; }, fetchCourseFailure: (state, { payload }) => { state.courseId = payload.courseId; - state.courseStatus = FAILED; + state.courseStatus = COURSE_FAILED; }, fetchCourseDenied: (state, { payload }) => { state.courseId = payload.courseId; - state.courseStatus = DENIED; + state.courseStatus = COURSE_DENIED; }, }, }); diff --git a/src/course/index.js b/src/active-course/index.js similarity index 73% rename from src/course/index.js rename to src/active-course/index.js index 41406344..d83c914a 100644 --- a/src/course/index.js +++ b/src/active-course/index.js @@ -1,10 +1,10 @@ export { activeCourseSelector } from './data/selectors'; export { reducer, - LOADING, - LOADED, - FAILED, - DENIED, + COURSE_LOADING, + COURSE_LOADED, + COURSE_FAILED, + COURSE_DENIED, fetchCourseRequest, fetchCourseSuccess, fetchCourseFailure, diff --git a/src/course-home/data/redux.test.js b/src/course-home/data/redux.test.js index 94a42f26..bad24807 100644 --- a/src/course-home/data/redux.test.js +++ b/src/course-home/data/redux.test.js @@ -1,16 +1,14 @@ import { Factory } from 'rosie'; import MockAdapter from 'axios-mock-adapter'; - import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth'; import { getConfig } from '@edx/frontend-platform'; -import * as thunks from './thunks'; - -import executeThunk from '../../utils'; - +import { COURSE_LOADED, COURSE_LOADING, COURSE_FAILED } from '../../active-course'; import initializeMockApp from '../../setupTest'; import initializeStore from '../../store'; -import { LOADING, FAILED } from '../../course'; +import executeThunk from '../../utils'; + +import * as thunks from './thunks'; const { loggingService } = initializeMockApp(); @@ -43,7 +41,7 @@ describe('Data layer integration tests', () => { it('Should initialize store', () => { expect(store.getState().activeCourse.courseId).toBeNull(); - expect(store.getState().activeCourse.courseStatus).toEqual(LOADING); + expect(store.getState().activeCourse.courseStatus).toEqual(COURSE_LOADING); }); describe('Test fetchDatesTab', () => { @@ -57,7 +55,7 @@ describe('Data layer integration tests', () => { await executeThunk(thunks.fetchDatesTab(courseId), store.dispatch); expect(loggingService.logError).toHaveBeenCalled(); - expect(store.getState().activeCourse.courseStatus).toEqual(FAILED); + expect(store.getState().activeCourse.courseStatus).toEqual(COURSE_FAILED); }); it('Should fetch, normalize, and save metadata', async () => { @@ -72,7 +70,7 @@ describe('Data layer integration tests', () => { await executeThunk(thunks.fetchDatesTab(courseId), store.dispatch); const state = store.getState(); - expect(state.activeCourse.courseStatus).toEqual('loaded'); + expect(state.activeCourse.courseStatus).toEqual(COURSE_LOADED); expect(state.activeCourse.courseId).toEqual(courseId); expect(state.courseHome.displayResetDatesToast).toBe(false); @@ -111,7 +109,7 @@ describe('Data layer integration tests', () => { await executeThunk(thunks.fetchOutlineTab(courseId), store.dispatch); expect(loggingService.logError).toHaveBeenCalled(); - expect(store.getState().activeCourse.courseStatus).toEqual('failed'); + expect(store.getState().activeCourse.courseStatus).toEqual(COURSE_FAILED); }); it('Should fetch, normalize, and save metadata', async () => { @@ -126,7 +124,7 @@ describe('Data layer integration tests', () => { await executeThunk(thunks.fetchOutlineTab(courseId), store.dispatch); const state = store.getState(); - expect(state.activeCourse.courseStatus).toEqual('loaded'); + expect(state.activeCourse.courseStatus).toEqual(COURSE_LOADED); expect(state.courseHome.displayResetDatesToast).toBe(false); // Validate course diff --git a/src/course-home/data/slice.js b/src/course-home/data/slice.js index 048abf45..69852d1f 100644 --- a/src/course-home/data/slice.js +++ b/src/course-home/data/slice.js @@ -1,10 +1,6 @@ /* eslint-disable no-param-reassign */ import { createSlice } from '@reduxjs/toolkit'; -export const LOADING = 'loading'; -export const LOADED = 'loaded'; -export const FAILED = 'failed'; - const slice = createSlice({ name: 'course-home', initialState: { diff --git a/src/course-home/data/thunks.js b/src/course-home/data/thunks.js index 589155bf..f0022098 100644 --- a/src/course-home/data/thunks.js +++ b/src/course-home/data/thunks.js @@ -17,7 +17,7 @@ import { fetchCourseRequest, fetchCourseSuccess, fetchCourseFailure, -} from '../../course'; +} from '../../active-course'; import { toggleResetDatesToast, diff --git a/src/courseware/CoursewareContainer.jsx b/src/courseware/CoursewareContainer.jsx index 3e3bd629..6b231852 100644 --- a/src/courseware/CoursewareContainer.jsx +++ b/src/courseware/CoursewareContainer.jsx @@ -13,15 +13,20 @@ import { fetchSequence, getResumeBlock, saveSequencePosition, + SEQUENCE_LOADED, + SEQUENCE_LOADING, + SEQUENCE_FAILED, } from './data'; import { TabPage } from '../tab-page'; +import { + activeCourseSelector, COURSE_LOADED, COURSE_LOADING, COURSE_FAILED, COURSE_DENIED, +} from '../active-course'; import Course from './course'; import { handleNextSectionCelebration } from './course/celebration'; -import { activeCourseSelector } from '../course'; const checkExamRedirect = memoize((sequenceStatus, sequence) => { - if (sequenceStatus === 'loaded') { + if (sequenceStatus === SEQUENCE_LOADED) { if (sequence.isTimeLimited && sequence.lmsWebUrl !== undefined) { global.location.assign(sequence.lmsWebUrl); } @@ -29,7 +34,7 @@ const checkExamRedirect = memoize((sequenceStatus, sequence) => { }); const checkResumeRedirect = memoize((courseStatus, courseId, sequenceId, firstSequenceId) => { - if (courseStatus === 'loaded' && !sequenceId) { + if (courseStatus === COURSE_LOADED && !sequenceId) { // Note that getResumeBlock is just an API call, not a redux thunk. getResumeBlock(courseId).then((data) => { // This is a replace because we don't want this change saved in the browser's history. @@ -43,7 +48,7 @@ const checkResumeRedirect = memoize((courseStatus, courseId, sequenceId, firstSe }); const checkContentRedirect = memoize((courseId, sequenceStatus, sequenceId, sequence, unitId) => { - if (sequenceStatus === 'loaded' && sequenceId && !unitId) { + if (sequenceStatus === SEQUENCE_LOADED && sequenceId && !unitId) { if (sequence.unitIds !== undefined && sequence.unitIds.length > 0) { const nextUnitId = sequence.unitIds[sequence.activeUnitIndex]; // This is a replace because we don't want this change saved in the browser's history. @@ -60,7 +65,7 @@ class CoursewareContainer extends Component { sequenceStatus, sequence, } = this.props; - if (sequenceStatus === 'loaded' && sequence.saveUnitPosition && unitId) { + if (sequenceStatus === SEQUENCE_LOADED && sequence.saveUnitPosition && unitId) { const activeUnitIndex = sequence.unitIds.indexOf(unitId); this.props.saveSequencePosition(courseId, sequenceId, activeUnitIndex); } @@ -208,7 +213,7 @@ class CoursewareContainer extends Component { }, } = this.props; - if (courseStatus === 'denied') { + if (courseStatus === COURSE_DENIED) { return this.renderDenied(); } @@ -258,8 +263,8 @@ CoursewareContainer.propTypes = { sequenceId: PropTypes.string, firstSequenceId: PropTypes.string, unitId: PropTypes.string, - courseStatus: PropTypes.oneOf(['loaded', 'loading', 'failed', 'denied']).isRequired, - sequenceStatus: PropTypes.oneOf(['loaded', 'loading', 'failed']).isRequired, + courseStatus: PropTypes.oneOf([COURSE_LOADED, COURSE_LOADING, COURSE_FAILED, COURSE_DENIED]).isRequired, + sequenceStatus: PropTypes.oneOf([SEQUENCE_LOADED, SEQUENCE_LOADING, SEQUENCE_FAILED]).isRequired, nextSequence: sequenceShape, previousSequence: sequenceShape, course: courseShape, @@ -292,7 +297,7 @@ const sequenceIdsSelector = createSelector( activeCourseSelector, (state) => state.models.sections, (courseStatus, course, sectionsById) => { - if (courseStatus !== 'loaded') { + if (courseStatus !== COURSE_LOADED) { return []; } const { sectionIds = [] } = course; @@ -333,7 +338,7 @@ const firstSequenceIdSelector = createSelector( activeCourseSelector, (state) => state.models.sections || {}, (courseStatus, course, sectionsById) => { - if (courseStatus !== 'loaded') { + if (courseStatus !== COURSE_LOADED) { return null; } const { sectionIds = [] } = course; diff --git a/src/courseware/course/CourseBreadcrumbs.jsx b/src/courseware/course/CourseBreadcrumbs.jsx index 67528127..84746795 100644 --- a/src/courseware/course/CourseBreadcrumbs.jsx +++ b/src/courseware/course/CourseBreadcrumbs.jsx @@ -6,6 +6,8 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { faHome } from '@fortawesome/free-solid-svg-icons'; import { useSelector } from 'react-redux'; import { useModel } from '../../generic/model-store'; +import { COURSE_LOADED } from '../../active-course'; +import { SEQUENCE_LOADED } from '../data'; function CourseBreadcrumb({ url, children, withSeparator, ...attrs @@ -44,7 +46,7 @@ export default function CourseBreadcrumbs({ const sequenceStatus = useSelector(state => state.courseware.sequenceStatus); const links = useMemo(() => { - if (courseStatus === 'loaded' && sequenceStatus === 'loaded') { + if (courseStatus === COURSE_LOADED && sequenceStatus === SEQUENCE_LOADED) { return [section, sequence].filter(node => !!node).map((node) => ({ id: node.id, label: node.title, diff --git a/src/courseware/course/bookmark/data/redux.test.js b/src/courseware/course/bookmark/data/redux.test.js index f0683613..3bd9bbf0 100644 --- a/src/courseware/course/bookmark/data/redux.test.js +++ b/src/courseware/course/bookmark/data/redux.test.js @@ -1,15 +1,18 @@ import MockAdapter from 'axios-mock-adapter'; - import { getAuthenticatedHttpClient, getAuthenticatedUser } from '@edx/frontend-platform/auth'; import { getConfig } from '@edx/frontend-platform'; -import * as thunks from './thunks'; - import executeThunk from '../../../../utils'; - import initializeMockApp from '../../../../setupTest'; import initializeStore from '../../../../store'; +import { + addBookmark, + removeBookmark, + BOOKMARK_FAILED, + BOOKMARK_LOADED, +} from './thunks'; + const { loggingService } = initializeMockApp(); const axiosMock = new MockAdapter(getAuthenticatedHttpClient()); @@ -32,24 +35,24 @@ describe('Data layer integration tests', () => { it('Should fail to create bookmark in case of error', async () => { axiosMock.onPost(createBookmarkURL).networkError(); - await executeThunk(thunks.addBookmark(unitId), store.dispatch); + await executeThunk(addBookmark(unitId), store.dispatch); expect(loggingService.logError).toHaveBeenCalled(); expect(axiosMock.history.post[0].url).toEqual(createBookmarkURL); expect(store.getState().models.units[unitId]).toEqual(expect.objectContaining({ bookmarked: false, - bookmarkedUpdateState: 'failed', + bookmarkedUpdateState: BOOKMARK_FAILED, })); }); it('Should create bookmark and update model state', async () => { axiosMock.onPost(createBookmarkURL).reply(201); - await executeThunk(thunks.addBookmark(unitId), store.dispatch); + await executeThunk(addBookmark(unitId), store.dispatch); expect(store.getState().models.units[unitId]).toEqual(expect.objectContaining({ bookmarked: true, - bookmarkedUpdateState: 'loaded', + bookmarkedUpdateState: BOOKMARK_LOADED, })); }); }); @@ -60,24 +63,24 @@ describe('Data layer integration tests', () => { it('Should fail to remove bookmark in case of error', async () => { axiosMock.onDelete(deleteBookmarkURL).networkError(); - await executeThunk(thunks.removeBookmark(unitId), store.dispatch); + await executeThunk(removeBookmark(unitId), store.dispatch); expect(loggingService.logError).toHaveBeenCalled(); expect(axiosMock.history.delete[0].url).toEqual(deleteBookmarkURL); expect(store.getState().models.units[unitId]).toEqual(expect.objectContaining({ bookmarked: true, - bookmarkedUpdateState: 'failed', + bookmarkedUpdateState: BOOKMARK_FAILED, })); }); it('Should delete bookmark and update model state', async () => { axiosMock.onDelete(deleteBookmarkURL).reply(201); - await executeThunk(thunks.removeBookmark(unitId), store.dispatch); + await executeThunk(removeBookmark(unitId), store.dispatch); expect(store.getState().models.units[unitId]).toEqual(expect.objectContaining({ bookmarked: false, - bookmarkedUpdateState: 'loaded', + bookmarkedUpdateState: BOOKMARK_LOADED, })); }); }); diff --git a/src/courseware/course/bookmark/data/thunks.js b/src/courseware/course/bookmark/data/thunks.js index 5bcd5141..24b17378 100644 --- a/src/courseware/course/bookmark/data/thunks.js +++ b/src/courseware/course/bookmark/data/thunks.js @@ -5,6 +5,10 @@ import { } from './api'; import { updateModel } from '../../../../generic/model-store'; +export const BOOKMARK_LOADING = 'loading'; +export const BOOKMARK_LOADED = 'loaded'; +export const BOOKMARK_FAILED = 'failed'; + export function addBookmark(unitId) { return async (dispatch) => { // Optimistically update the bookmarked flag. @@ -13,7 +17,7 @@ export function addBookmark(unitId) { model: { id: unitId, bookmarked: true, - bookmarkedUpdateState: 'loading', + bookmarkedUpdateState: BOOKMARK_LOADING, }, })); @@ -24,7 +28,7 @@ export function addBookmark(unitId) { model: { id: unitId, bookmarked: true, - bookmarkedUpdateState: 'loaded', + bookmarkedUpdateState: BOOKMARK_LOADED, }, })); } catch (error) { @@ -34,7 +38,7 @@ export function addBookmark(unitId) { model: { id: unitId, bookmarked: false, - bookmarkedUpdateState: 'failed', + bookmarkedUpdateState: BOOKMARK_FAILED, }, })); } @@ -49,7 +53,7 @@ export function removeBookmark(unitId) { model: { id: unitId, bookmarked: false, - bookmarkedUpdateState: 'loading', + bookmarkedUpdateState: BOOKMARK_LOADING, }, })); try { @@ -59,7 +63,7 @@ export function removeBookmark(unitId) { model: { id: unitId, bookmarked: false, - bookmarkedUpdateState: 'loaded', + bookmarkedUpdateState: BOOKMARK_LOADED, }, })); } catch (error) { @@ -69,7 +73,7 @@ export function removeBookmark(unitId) { model: { id: unitId, bookmarked: true, - bookmarkedUpdateState: 'failed', + bookmarkedUpdateState: BOOKMARK_FAILED, }, })); } diff --git a/src/courseware/course/bookmark/index.js b/src/courseware/course/bookmark/index.js index 23353466..2f992bb8 100644 --- a/src/courseware/course/bookmark/index.js +++ b/src/courseware/course/bookmark/index.js @@ -1,3 +1,9 @@ export { default as BookmarkButton } from './BookmarkButton'; export { default as BookmarkFilledIcon } from './BookmarkFilledIcon'; export { default as BookmarkOutlineIcon } from './BookmarkFilledIcon'; + +export { + BOOKMARK_LOADING, + BOOKMARK_LOADED, + BOOKMARK_FAILED, +} from './data/thunks'; diff --git a/src/courseware/course/sequence/Sequence.jsx b/src/courseware/course/sequence/Sequence.jsx index 2f1c92c5..bd1c0e1a 100644 --- a/src/courseware/course/sequence/Sequence.jsx +++ b/src/courseware/course/sequence/Sequence.jsx @@ -15,6 +15,7 @@ import CourseLicense from '../course-license'; import messages from './messages'; import { SequenceNavigation, UnitNavigation } from './sequence-navigation'; import SequenceContent from './SequenceContent'; +import { SEQUENCE_LOADED, SEQUENCE_LOADING } from '../../data'; function Sequence({ unitId, @@ -73,7 +74,7 @@ function Sequence({ const { add, remove } = useContext(UserMessagesContext); useEffect(() => { let id = null; - if (sequenceStatus === 'loaded') { + if (sequenceStatus === SEQUENCE_LOADED) { if (sequence.bannerText) { id = add({ code: null, @@ -101,7 +102,7 @@ function Sequence({ } }, [unit]); - if (sequenceStatus === 'loading') { + if (sequenceStatus === SEQUENCE_LOADING) { if (!sequenceId) { return (