diff --git a/src/course-home/data/api.js b/src/course-home/data/api.js index 22b6738df4..35f0079b4c 100644 --- a/src/course-home/data/api.js +++ b/src/course-home/data/api.js @@ -403,6 +403,12 @@ export async function getOutlineTabData(courseId) { }; } +export async function getOutlineBlocksData(courseId) { + const url = `${getConfig().LMS_BASE_URL}/api/course_home/v1/outline_blocks/${courseId}`; + const { data } = await getAuthenticatedHttpClient().get(url); + return data; +} + export async function postCourseDeadlines(courseId, model) { const url = new URL(`${getConfig().LMS_BASE_URL}/api/course_experience/v1/reset_course_deadlines`); return getAuthenticatedHttpClient().post(url.href, { diff --git a/src/course-home/data/index.js b/src/course-home/data/index.js index c315d84eb2..920ef4e528 100644 --- a/src/course-home/data/index.js +++ b/src/course-home/data/index.js @@ -1,6 +1,7 @@ export { fetchDatesTab, fetchOutlineTab, + fetchOutlineBlocks, fetchProgressTab, resetDeadlines, deprecatedSaveCourseGoal, diff --git a/src/course-home/data/slice.js b/src/course-home/data/slice.js index 21c804d3f3..ca6981ff60 100644 --- a/src/course-home/data/slice.js +++ b/src/course-home/data/slice.js @@ -18,6 +18,8 @@ const slice = createSlice({ toastBodyLink: null, toastHeader: '', showSearch: false, + outlineBlocksStatus: 'idle', + outlineBlocks: null, }, reducers: { fetchProctoringInfoResolved: (state) => { @@ -34,6 +36,8 @@ const slice = createSlice({ fetchTabRequest: (state, { payload }) => { state.courseId = payload.courseId; state.courseStatus = LOADING; + state.outlineBlocksStatus = 'idle'; + state.outlineBlocks = null; }, fetchTabSuccess: (state, { payload }) => { state.courseId = payload.courseId; @@ -53,6 +57,16 @@ const slice = createSlice({ setShowSearch: (state, { payload }) => { state.showSearch = payload; }, + fetchOutlineBlocksRequest: (state) => { + state.outlineBlocksStatus = LOADING; + }, + fetchOutlineBlocksSuccess: (state, { payload }) => { + state.outlineBlocksStatus = LOADED; + state.outlineBlocks = payload; + }, + fetchOutlineBlocksFailure: (state) => { + state.outlineBlocksStatus = FAILED; + }, }, }); @@ -64,6 +78,9 @@ export const { fetchTabSuccess, setCallToActionToast, setShowSearch, + fetchOutlineBlocksRequest, + fetchOutlineBlocksSuccess, + fetchOutlineBlocksFailure, } = slice.actions; export const { diff --git a/src/course-home/data/thunks.js b/src/course-home/data/thunks.js index ec0567f9cf..9f1f5a0576 100644 --- a/src/course-home/data/thunks.js +++ b/src/course-home/data/thunks.js @@ -5,6 +5,7 @@ import { getCourseHomeCourseMetadata, getDatesTabData, getOutlineTabData, + getOutlineBlocksData, getProgressTabData, postCourseDeadlines, deprecatedPostCourseGoals, @@ -14,6 +15,7 @@ import { getLiveTabIframe, getCoursewareSearchEnabledFlag, searchCourseContentFromAPI, + normalizeOutlineBlocks, } from './api'; import { @@ -26,6 +28,9 @@ import { fetchTabRequest, fetchTabSuccess, setCallToActionToast, + fetchOutlineBlocksRequest, + fetchOutlineBlocksSuccess, + fetchOutlineBlocksFailure, } from './slice'; import mapSearchResponse from '../courseware-search/map-search-response'; @@ -105,6 +110,55 @@ export function fetchDiscussionTab(courseId) { return fetchTab(courseId, 'discussion'); } +export function fetchOutlineBlocks(courseId) { + return async (dispatch, getState) => { + const { outlineBlocksStatus } = getState().courseHome; + if (outlineBlocksStatus === 'loaded' || outlineBlocksStatus === 'loading') { + return; + } + dispatch(fetchOutlineBlocksRequest()); + try { + const data = await getOutlineBlocksData(courseId); + dispatch(fetchOutlineBlocksSuccess(data.blocks)); + + const normalized = normalizeOutlineBlocks(courseId, data.blocks); + const outlineModel = getState().models.outline?.[courseId]; + if (!outlineModel) { return; } + + const mergedSections = { ...outlineModel.courseBlocks.sections }; + Object.keys(mergedSections).forEach(sectionId => { + const blockData = normalized.sections[sectionId]; + if (blockData) { + mergedSections[sectionId] = { ...mergedSections[sectionId], ...blockData }; + } + }); + + const mergedSequences = { ...outlineModel.courseBlocks.sequences }; + Object.keys(mergedSequences).forEach(seqId => { + const blockData = normalized.sequences[seqId]; + if (blockData) { + mergedSequences[seqId] = { ...mergedSequences[seqId], ...blockData }; + } + }); + + dispatch(updateModel({ + modelType: 'outline', + model: { + id: courseId, + courseBlocks: { + ...outlineModel.courseBlocks, + sections: mergedSections, + sequences: mergedSequences, + }, + }, + })); + } catch (e) { + dispatch(fetchOutlineBlocksFailure()); + logError(e); + } + }; +} + export function dismissWelcomeMessage(courseId) { return async () => postDismissWelcomeMessage(courseId); } diff --git a/src/course-home/outline-tab/OutlineTab.jsx b/src/course-home/outline-tab/OutlineTab.jsx index eb5cd97951..a1a2c6dbfc 100644 --- a/src/course-home/outline-tab/OutlineTab.jsx +++ b/src/course-home/outline-tab/OutlineTab.jsx @@ -1,6 +1,6 @@ import React, { useEffect, useState } from 'react'; import { useLocation, useNavigate } from 'react-router-dom'; -import { useSelector } from 'react-redux'; +import { useSelector, useDispatch } from 'react-redux'; import { sendTrackEvent } from '@edx/frontend-platform/analytics'; import { getAuthenticatedUser } from '@edx/frontend-platform/auth'; import { injectIntl, intlShape } from '@edx/frontend-platform/i18n'; @@ -13,7 +13,7 @@ import CourseHandouts from './widgets/CourseHandouts'; import StartOrResumeCourseCard from './widgets/StartOrResumeCourseCard'; import WeeklyLearningGoalCard from './widgets/WeeklyLearningGoalCard'; import CourseTools from './widgets/CourseTools'; -import { fetchOutlineTab } from '../data'; +import { fetchOutlineTab, fetchOutlineBlocks } from '../data'; import messages from './messages'; import Section from './Section'; import ShiftDatesAlert from '../suggested-schedule-messaging/ShiftDatesAlert'; @@ -98,6 +98,7 @@ const OutlineTab = ({ intl }) => { } = useModel('coursewareMeta', courseId); const [expandAll, setExpandAll] = useState(false); + const dispatch = useDispatch(); const navigate = useNavigate(); const eventProperties = { @@ -195,7 +196,14 @@ const OutlineTab = ({ intl }) => { <>