Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions src/course-home/data/api.js
Original file line number Diff line number Diff line change
Expand Up @@ -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, {
Expand Down
1 change: 1 addition & 0 deletions src/course-home/data/index.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
export {
fetchDatesTab,
fetchOutlineTab,
fetchOutlineBlocks,
fetchProgressTab,
resetDeadlines,
deprecatedSaveCourseGoal,
Expand Down
17 changes: 17 additions & 0 deletions src/course-home/data/slice.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ const slice = createSlice({
toastBodyLink: null,
toastHeader: '',
showSearch: false,
outlineBlocksStatus: 'idle',
outlineBlocks: null,
},
reducers: {
fetchProctoringInfoResolved: (state) => {
Expand All @@ -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;
Expand All @@ -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;
},
},
});

Expand All @@ -64,6 +78,9 @@ export const {
fetchTabSuccess,
setCallToActionToast,
setShowSearch,
fetchOutlineBlocksRequest,
fetchOutlineBlocksSuccess,
fetchOutlineBlocksFailure,
} = slice.actions;

export const {
Expand Down
54 changes: 54 additions & 0 deletions src/course-home/data/thunks.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
getCourseHomeCourseMetadata,
getDatesTabData,
getOutlineTabData,
getOutlineBlocksData,
getProgressTabData,
postCourseDeadlines,
deprecatedPostCourseGoals,
Expand All @@ -14,6 +15,7 @@ import {
getLiveTabIframe,
getCoursewareSearchEnabledFlag,
searchCourseContentFromAPI,
normalizeOutlineBlocks,
} from './api';

import {
Expand All @@ -26,6 +28,9 @@ import {
fetchTabRequest,
fetchTabSuccess,
setCallToActionToast,
fetchOutlineBlocksRequest,
fetchOutlineBlocksSuccess,
fetchOutlineBlocksFailure,
} from './slice';

import mapSearchResponse from '../courseware-search/map-search-response';
Expand Down Expand Up @@ -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);
}
Expand Down
14 changes: 11 additions & 3 deletions src/course-home/outline-tab/OutlineTab.jsx
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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';
Expand Down Expand Up @@ -98,6 +98,7 @@ const OutlineTab = ({ intl }) => {
} = useModel('coursewareMeta', courseId);

const [expandAll, setExpandAll] = useState(false);
const dispatch = useDispatch();
const navigate = useNavigate();

const eventProperties = {
Expand Down Expand Up @@ -195,7 +196,14 @@ const OutlineTab = ({ intl }) => {
<>
<div className="row w-100 m-0 mb-3 justify-content-end">
<div className="col-12 col-md-auto p-0">
<Button variant="outline-primary" block onClick={() => { setExpandAll(!expandAll); }}>
<Button
variant="outline-primary"
block
onClick={() => {
if (!expandAll) { dispatch(fetchOutlineBlocks(courseId)); }
setExpandAll(!expandAll);
}}
>
{expandAll ? intl.formatMessage(messages.collapseAll) : intl.formatMessage(messages.expandAll)}
</Button>
</div>
Expand Down
8 changes: 7 additions & 1 deletion src/course-home/outline-tab/Section.jsx
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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) {
Expand Down Expand Up @@ -63,6 +65,7 @@ const Section = ({
},
} = useModel('outline', courseId);

const dispatch = useDispatch();
const [open, setOpen] = useState(defaultOpen);

useEffect(() => {
Expand Down Expand Up @@ -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={(
<IconButton
alt={intl.formatMessage(messages.openSection)}
Expand Down
4 changes: 4 additions & 0 deletions src/course-home/outline-tab/SequenceLink.jsx
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -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) {
Expand Down Expand Up @@ -66,6 +68,7 @@ const SequenceLink = ({
const {
userTimezone,
} = useModel('outline', courseId);
const { outlineBlocksStatus } = useSelector(state => state.courseHome);

const timezoneFormatArgs = userTimezone ? { timeZone: userTimezone } : {};

Expand Down Expand Up @@ -141,6 +144,7 @@ const SequenceLink = ({
</div>
<div className="col-10 p-0 ml-3 text-break">
<span className="align-middle">{displayTitle}</span>
{outlineBlocksStatus === 'loading' && <span className="loading-dots" />}
<span className="sr-only">
, {intl.formatMessage(complete ? messages.completedAssignment : messages.incompleteAssignment)}
</span>
Expand Down
19 changes: 19 additions & 0 deletions src/course-home/outline-tab/SequenceLink.scss
Original file line number Diff line number Diff line change
@@ -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: ''; }
}
1 change: 1 addition & 0 deletions src/index.scss
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down