From 0c4b5200af71ac7c0ae7032668333b7c7be8a4a6 Mon Sep 17 00:00:00 2001 From: tenkalden Date: Thu, 12 Feb 2026 15:43:39 +0530 Subject: [PATCH 1/2] addeed API endpoint and thunk to fetch and merge outline blocks(for lazy loading) data on section expand --- src/course-home/data/api.js | 6 +++ src/course-home/data/index.js | 1 + src/course-home/data/slice.js | 17 +++++++ src/course-home/data/thunks.js | 54 ++++++++++++++++++++++ src/course-home/outline-tab/OutlineTab.jsx | 14 ++++-- src/course-home/outline-tab/Section.jsx | 8 +++- 6 files changed, 96 insertions(+), 4 deletions(-) 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 }) => { <>
-
diff --git a/src/course-home/outline-tab/Section.jsx b/src/course-home/outline-tab/Section.jsx index fd4c8e1d42..c465ac9d9f 100644 --- a/src/course-home/outline-tab/Section.jsx +++ b/src/course-home/outline-tab/Section.jsx @@ -1,5 +1,6 @@ import React, { useEffect, useState } from 'react'; import PropTypes from 'prop-types'; +import { useDispatch } from 'react-redux'; import { injectIntl, intlShape } from '@edx/frontend-platform/i18n'; import { Collapsible, IconButton, Icon } from '@openedx/paragon'; import { faCheckCircle as fasCheckCircle, faMinus, faPlus } from '@fortawesome/free-solid-svg-icons'; @@ -12,6 +13,7 @@ import { useModel } from '../../generic/model-store'; import genericMessages from '../../generic/messages'; import messages from './messages'; +import { fetchOutlineBlocks } from '../data'; const renderTibetanText = (text) => { if (!text) { @@ -63,6 +65,7 @@ const Section = ({ }, } = useModel('outline', courseId); + const dispatch = useDispatch(); const [open, setOpen] = useState(defaultOpen); useEffect(() => { @@ -123,7 +126,10 @@ const Section = ({ styling="card-lg" title={sectionTitle} open={open} - onToggle={() => { setOpen(!open); }} + onToggle={() => { + if (!open) { dispatch(fetchOutlineBlocks(courseId)); } + setOpen(!open); + }} iconWhenClosed={( Date: Thu, 12 Feb 2026 16:46:04 +0530 Subject: [PATCH 2/2] add loading indicator to subsection during outline blocks fetch --- src/course-home/outline-tab/SequenceLink.jsx | 4 ++++ src/course-home/outline-tab/SequenceLink.scss | 19 +++++++++++++++++++ src/index.scss | 1 + 3 files changed, 24 insertions(+) create mode 100644 src/course-home/outline-tab/SequenceLink.scss diff --git a/src/course-home/outline-tab/SequenceLink.jsx b/src/course-home/outline-tab/SequenceLink.jsx index 996ab492e0..18dab7534b 100644 --- a/src/course-home/outline-tab/SequenceLink.jsx +++ b/src/course-home/outline-tab/SequenceLink.jsx @@ -1,5 +1,6 @@ import PropTypes from 'prop-types'; import classNames from 'classnames'; +import { useSelector } from 'react-redux'; import { Link } from 'react-router-dom'; import { FormattedMessage, @@ -16,6 +17,7 @@ import { Block } from '@openedx/paragon/icons'; import EffortEstimate from '../../shared/effort-estimate'; import { useModel } from '../../generic/model-store'; import messages from './messages'; +import './SequenceLink.scss'; const renderTibetanText = (text) => { if (!text) { @@ -66,6 +68,7 @@ const SequenceLink = ({ const { userTimezone, } = useModel('outline', courseId); + const { outlineBlocksStatus } = useSelector(state => state.courseHome); const timezoneFormatArgs = userTimezone ? { timeZone: userTimezone } : {}; @@ -141,6 +144,7 @@ const SequenceLink = ({
{displayTitle} + {outlineBlocksStatus === 'loading' && } , {intl.formatMessage(complete ? messages.completedAssignment : messages.incompleteAssignment)} diff --git a/src/course-home/outline-tab/SequenceLink.scss b/src/course-home/outline-tab/SequenceLink.scss new file mode 100644 index 0000000000..3a3a8eb1ec --- /dev/null +++ b/src/course-home/outline-tab/SequenceLink.scss @@ -0,0 +1,19 @@ +.loading-dots { + display: inline-block; + margin-left: 0.25rem; + font-weight: bold; + color: #999; +} + +.loading-dots::after { + content: ''; + animation: dots 1.2s steps(4, end) infinite; +} + +@keyframes dots { + 0% { content: ''; } + 25% { content: '.'; } + 50% { content: '..'; } + 75% { content: '...'; } + 100% { content: ''; } +} diff --git a/src/index.scss b/src/index.scss index c05f9e7985..8913ec217a 100755 --- a/src/index.scss +++ b/src/index.scss @@ -459,6 +459,7 @@ @import "generic/upgrade-notification/UpgradeNotification.scss"; @import "generic/upsell-bullets/UpsellBullets.scss"; @import "course-home/outline-tab/widgets/ProctoringInfoPanel.scss"; +@import "course-home/outline-tab/SequenceLink.scss"; @import "course-home/outline-tab/widgets/FlagButton.scss"; @import "course-home/progress-tab/course-completion/CompletionDonutChart.scss"; @import "course-home/progress-tab/grades/course-grade/GradeBar.scss";