fix: replaced the LMS endpoint for navigating the course unit page
fix: [AXIMST-424] Course unit - Fixed network connection behavior (#138) * fix: [AXIMST-424] fixed network connetcion behavior * fix: added placeholder for unsuccessful loading for the page * refactor: code refactoring
This commit is contained in:
committed by
Adolfo R. Brandes
parent
d76aaa73a4
commit
8acd27d7bf
@@ -13,6 +13,7 @@ import getPageHeadTitle from '../generic/utils';
|
||||
import AlertMessage from '../generic/alert-message';
|
||||
import ProcessingNotification from '../generic/processing-notification';
|
||||
import InternetConnectionAlert from '../generic/internet-connection-alert';
|
||||
import ConnectionErrorAlert from '../generic/ConnectionErrorAlert';
|
||||
import Loading from '../generic/Loading';
|
||||
import AddComponent from './add-component/AddComponent';
|
||||
import CourseXBlock from './course-xblock/CourseXBlock';
|
||||
@@ -32,6 +33,7 @@ const CourseUnit = ({ courseId }) => {
|
||||
sequenceId,
|
||||
unitTitle,
|
||||
isQueryPending,
|
||||
sequenceStatus,
|
||||
savingStatus,
|
||||
isTitleEditFormOpen,
|
||||
isErrorAlert,
|
||||
@@ -57,6 +59,14 @@ const CourseUnit = ({ courseId }) => {
|
||||
return <Loading />;
|
||||
}
|
||||
|
||||
if (sequenceStatus === RequestStatus.FAILED) {
|
||||
return (
|
||||
<Container size="xl" className="course-unit px-4 mt-4">
|
||||
<ConnectionErrorAlert />
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Container size="xl" className="course-unit px-4">
|
||||
|
||||
@@ -289,6 +289,10 @@ module.exports = {
|
||||
},
|
||||
},
|
||||
],
|
||||
course_sequence_ids: [
|
||||
'block-v1:edx+876+2030+type@sequential+block@297321078a0f4c26a50d671ed87642a6',
|
||||
'block-v1:edx+876+2030+type@sequential+block@4e91bdfefd8e4173a03d19c4d91e1936',
|
||||
],
|
||||
xblock_info: {
|
||||
id: 'block-v1:edX+DemoX+Demo_Course+type@vertical+block@867dddb6f55d410caaa9c1eb9c6743ec',
|
||||
display_name: 'Getting Started',
|
||||
|
||||
@@ -3,20 +3,14 @@ import { useLayoutEffect, useRef, useState } from 'react';
|
||||
import { useWindowSize } from '@openedx/paragon';
|
||||
|
||||
import { useModel } from '../../generic/model-store';
|
||||
import {
|
||||
getCourseSectionVertical,
|
||||
getCourseUnit,
|
||||
sequenceIdsSelector,
|
||||
} from '../data/selectors';
|
||||
import { getCourseSectionVertical, getSequenceIds } from '../data/selectors';
|
||||
|
||||
export function useSequenceNavigationMetadata(currentSequenceId, currentUnitId) {
|
||||
const sequenceIds = useSelector(sequenceIdsSelector);
|
||||
export function useSequenceNavigationMetadata(courseId, currentSequenceId, currentUnitId) {
|
||||
const { nextUrl, prevUrl } = useSelector(getCourseSectionVertical);
|
||||
const sequence = useModel('sequences', currentSequenceId);
|
||||
const { courseId } = useSelector(getCourseUnit);
|
||||
const isFirstUnit = !prevUrl;
|
||||
const isLastUnit = !nextUrl;
|
||||
|
||||
const sequenceIds = useSelector(getSequenceIds);
|
||||
const sequenceIndex = sequenceIds.indexOf(currentSequenceId);
|
||||
const unitIndex = sequence.unitIds.indexOf(currentUnitId);
|
||||
|
||||
|
||||
@@ -20,6 +20,7 @@ import SequenceNavigationTabs from './SequenceNavigationTabs';
|
||||
|
||||
const SequenceNavigation = ({
|
||||
intl,
|
||||
courseId,
|
||||
unitId,
|
||||
sequenceId,
|
||||
className,
|
||||
@@ -28,7 +29,7 @@ const SequenceNavigation = ({
|
||||
const sequenceStatus = useSelector(getSequenceStatus);
|
||||
const {
|
||||
isFirstUnit, isLastUnit, nextLink, previousLink,
|
||||
} = useSequenceNavigationMetadata(sequenceId, unitId);
|
||||
} = useSequenceNavigationMetadata(courseId, sequenceId, unitId);
|
||||
const sequence = useModel('sequences', sequenceId);
|
||||
|
||||
const shouldDisplayNotificationTriggerInSequence = useWindowSize().width < breakpoints.small.minWidth;
|
||||
@@ -104,6 +105,7 @@ const SequenceNavigation = ({
|
||||
|
||||
SequenceNavigation.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
courseId: PropTypes.string.isRequired,
|
||||
unitId: PropTypes.string,
|
||||
className: PropTypes.string,
|
||||
sequenceId: PropTypes.string,
|
||||
|
||||
@@ -3,23 +3,13 @@ import { camelCaseObject, getConfig } from '@edx/frontend-platform';
|
||||
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
|
||||
|
||||
import { PUBLISH_TYPES } from '../constants';
|
||||
import {
|
||||
normalizeLearningSequencesData,
|
||||
normalizeMetadata,
|
||||
normalizeCourseHomeCourseMetadata,
|
||||
appendBrowserTimezoneToUrl,
|
||||
normalizeCourseSectionVerticalData,
|
||||
} from './utils';
|
||||
import { normalizeCourseSectionVerticalData } from './utils';
|
||||
|
||||
const getStudioBaseUrl = () => getConfig().STUDIO_BASE_URL;
|
||||
const getLmsBaseUrl = () => getConfig().LMS_BASE_URL;
|
||||
|
||||
export const getCourseUnitApiUrl = (itemId) => `${getStudioBaseUrl()}/xblock/container/${itemId}`;
|
||||
export const getXBlockBaseApiUrl = (itemId) => `${getStudioBaseUrl()}/xblock/${itemId}`;
|
||||
export const getCourseSectionVerticalApiUrl = (itemId) => `${getStudioBaseUrl()}/api/contentstore/v1/container_handler/${itemId}`;
|
||||
export const getLearningSequencesOutlineApiUrl = (courseId) => `${getLmsBaseUrl()}/api/learning_sequences/v1/course_outline/${courseId}`;
|
||||
export const getCourseMetadataApiUrl = (courseId) => `${getLmsBaseUrl()}/api/courseware/course/${courseId}`;
|
||||
export const getCourseHomeCourseMetadataApiUrl = (courseId) => `${getLmsBaseUrl()}/api/course_home/course_metadata/${courseId}`;
|
||||
export const getCourseVerticalChildrenApiUrl = (itemId) => `${getStudioBaseUrl()}/api/contentstore/v1/container/vertical/${itemId}/children`;
|
||||
|
||||
export const postXBlockBaseApiUrl = () => `${getStudioBaseUrl()}/xblock/`;
|
||||
@@ -65,45 +55,6 @@ export async function getCourseSectionVerticalData(unitId) {
|
||||
return normalizeCourseSectionVerticalData(data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the outline of learning sequences for a specific course.
|
||||
* @param {string} courseId - The ID of the course.
|
||||
* @returns {Promise<Object>} A Promise that resolves to the normalized learning sequences outline data.
|
||||
*/
|
||||
export async function getLearningSequencesOutline(courseId) {
|
||||
const { href } = new URL(getLearningSequencesOutlineApiUrl(courseId));
|
||||
const { data } = await getAuthenticatedHttpClient().get(href, {});
|
||||
|
||||
return normalizeLearningSequencesData(data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves metadata for a specific course.
|
||||
* @param {string} courseId - The ID of the course.
|
||||
* @returns {Promise<Object>} A Promise that resolves to the normalized course metadata.
|
||||
*/
|
||||
export async function getCourseMetadata(courseId) {
|
||||
let courseMetadataApiUrl = getCourseMetadataApiUrl(courseId);
|
||||
courseMetadataApiUrl = appendBrowserTimezoneToUrl(courseMetadataApiUrl);
|
||||
const metadata = await getAuthenticatedHttpClient().get(courseMetadataApiUrl);
|
||||
|
||||
return normalizeMetadata(metadata);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves metadata for a course's home page.
|
||||
* @param {string} courseId - The ID of the course.
|
||||
* @param {string} rootSlug - The root slug for the course.
|
||||
* @returns {Promise<Object>} A Promise that resolves to the normalized course home page metadata.
|
||||
*/
|
||||
export async function getCourseHomeCourseMetadata(courseId, rootSlug) {
|
||||
let courseHomeCourseMetadataApiUrl = getCourseHomeCourseMetadataApiUrl(courseId);
|
||||
courseHomeCourseMetadataApiUrl = appendBrowserTimezoneToUrl(courseHomeCourseMetadataApiUrl);
|
||||
const { data } = await getAuthenticatedHttpClient().get(courseHomeCourseMetadataApiUrl);
|
||||
|
||||
return normalizeCourseHomeCourseMetadata(data, rootSlug);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new course XBlock.
|
||||
* @param {Object} options - The options for creating the XBlock.
|
||||
|
||||
@@ -1,30 +1,9 @@
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
|
||||
import { RequestStatus } from '../../data/constants';
|
||||
|
||||
export const getCourseUnitData = (state) => state.courseUnit.unit;
|
||||
export const getCourseUnit = (state) => state.courseUnit;
|
||||
export const getSavingStatus = (state) => state.courseUnit.savingStatus;
|
||||
export const getLoadingStatus = (state) => state.courseUnit.loadingStatus;
|
||||
export const getSequenceStatus = (state) => state.courseUnit.sequenceStatus;
|
||||
export const getSequenceIds = (state) => state.courseUnit.courseSectionVertical.courseSequenceIds;
|
||||
export const getCourseSectionVertical = (state) => state.courseUnit.courseSectionVertical;
|
||||
export const getCourseUnitComponentTemplates = (state) => state.courseUnit.courseSectionVertical.componentTemplates;
|
||||
export const getCourseSectionVerticalLoadingStatus = (state) => state
|
||||
.courseUnit.loadingStatus.courseSectionVerticalLoadingStatus;
|
||||
export const getCourseStatus = state => state.courseUnit.courseStatus;
|
||||
export const getCoursewareMeta = state => state.models.coursewareMeta;
|
||||
export const getSections = state => state.models.sections;
|
||||
export const getCourseId = state => state.courseDetail.courseId;
|
||||
export const getSequenceId = state => state.courseUnit.sequenceId;
|
||||
export const getCourseVerticalChildren = state => state.courseUnit.courseVerticalChildren;
|
||||
export const sequenceIdsSelector = createSelector(
|
||||
[getCourseStatus, getCoursewareMeta, getSections, getCourseId],
|
||||
(courseStatus, coursewareMeta, sections, courseId) => {
|
||||
if (courseStatus !== RequestStatus.SUCCESSFUL) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const sectionIds = coursewareMeta[courseId].sectionIds || [];
|
||||
return sectionIds.flatMap(sectionId => sections[sectionId].sequenceIds);
|
||||
},
|
||||
);
|
||||
export const getCourseId = (state) => state.courseDetail.courseId;
|
||||
export const getSequenceId = (state) => state.courseUnit.sequenceId;
|
||||
export const getCourseVerticalChildren = (state) => state.courseUnit.courseVerticalChildren;
|
||||
|
||||
@@ -52,22 +52,6 @@ const slice = createSlice({
|
||||
state.sequenceStatus = RequestStatus.FAILED;
|
||||
state.sequenceMightBeUnit = payload.sequenceMightBeUnit || false;
|
||||
},
|
||||
fetchCourseRequest: (state, { payload }) => {
|
||||
state.courseId = payload.courseId;
|
||||
state.courseStatus = RequestStatus.IN_PROGRESS;
|
||||
},
|
||||
fetchCourseSuccess: (state, { payload }) => {
|
||||
state.courseId = payload.courseId;
|
||||
state.courseStatus = RequestStatus.SUCCESSFUL;
|
||||
},
|
||||
fetchCourseFailure: (state, { payload }) => {
|
||||
state.courseId = payload.courseId;
|
||||
state.courseStatus = RequestStatus.FAILED;
|
||||
},
|
||||
fetchCourseDenied: (state, { payload }) => {
|
||||
state.courseId = payload.courseId;
|
||||
state.courseStatus = RequestStatus.DENIED;
|
||||
},
|
||||
fetchCourseSectionVerticalDataSuccess: (state, { payload }) => {
|
||||
state.courseSectionVertical = payload;
|
||||
},
|
||||
@@ -122,10 +106,6 @@ export const {
|
||||
fetchSequenceRequest,
|
||||
fetchSequenceSuccess,
|
||||
fetchSequenceFailure,
|
||||
fetchCourseRequest,
|
||||
fetchCourseSuccess,
|
||||
fetchCourseFailure,
|
||||
fetchCourseDenied,
|
||||
fetchCourseSectionVerticalDataSuccess,
|
||||
updateLoadingCourseSectionVerticalDataStatus,
|
||||
changeEditTitleFormOpen,
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { logError, logInfo } from '@edx/frontend-platform/logging';
|
||||
import { camelCaseObject } from '@edx/frontend-platform';
|
||||
|
||||
import {
|
||||
@@ -7,15 +6,10 @@ import {
|
||||
} from '../../generic/processing-notification/data/slice';
|
||||
import { RequestStatus } from '../../data/constants';
|
||||
import { NOTIFICATION_MESSAGES } from '../../constants';
|
||||
import {
|
||||
addModel, updateModel, updateModels, updateModelsMap, addModelsMap,
|
||||
} from '../../generic/model-store';
|
||||
import { updateModel, updateModels } from '../../generic/model-store';
|
||||
import {
|
||||
getCourseUnitData,
|
||||
editUnitDisplayName,
|
||||
getCourseMetadata,
|
||||
getLearningSequencesOutline,
|
||||
getCourseHomeCourseMetadata,
|
||||
getCourseSectionVerticalData,
|
||||
createCourseXblock,
|
||||
getCourseVerticalChildren,
|
||||
@@ -30,10 +24,6 @@ import {
|
||||
fetchSequenceRequest,
|
||||
fetchSequenceFailure,
|
||||
fetchSequenceSuccess,
|
||||
fetchCourseRequest,
|
||||
fetchCourseSuccess,
|
||||
fetchCourseDenied,
|
||||
fetchCourseFailure,
|
||||
fetchCourseSectionVerticalDataSuccess,
|
||||
updateLoadingCourseSectionVerticalDataStatus,
|
||||
updateLoadingCourseXblockStatus,
|
||||
@@ -146,88 +136,6 @@ export function editCourseUnitVisibilityAndData(itemId, type, isVisible) {
|
||||
};
|
||||
}
|
||||
|
||||
export function fetchCourse(courseId) {
|
||||
return async (dispatch) => {
|
||||
dispatch(fetchCourseRequest({ courseId }));
|
||||
Promise.allSettled([
|
||||
getCourseMetadata(courseId),
|
||||
getLearningSequencesOutline(courseId),
|
||||
getCourseHomeCourseMetadata(courseId, 'courseware'),
|
||||
]).then(([
|
||||
courseMetadataResult,
|
||||
learningSequencesOutlineResult,
|
||||
courseHomeMetadataResult]) => {
|
||||
if (courseMetadataResult.status === 'fulfilled') {
|
||||
dispatch(addModel({
|
||||
modelType: 'coursewareMeta',
|
||||
model: courseMetadataResult.value,
|
||||
}));
|
||||
}
|
||||
|
||||
if (courseHomeMetadataResult.status === 'fulfilled') {
|
||||
dispatch(addModel({
|
||||
modelType: 'courseHomeMeta',
|
||||
model: {
|
||||
id: courseId,
|
||||
...courseHomeMetadataResult.value,
|
||||
},
|
||||
}));
|
||||
}
|
||||
|
||||
if (learningSequencesOutlineResult.status === 'fulfilled') {
|
||||
const { courses, sections } = learningSequencesOutlineResult.value;
|
||||
|
||||
// This updates the course with a sectionIds array from the Learning Sequence data.
|
||||
dispatch(updateModelsMap({
|
||||
modelType: 'coursewareMeta',
|
||||
modelsMap: courses,
|
||||
}));
|
||||
dispatch(addModelsMap({
|
||||
modelType: 'sections',
|
||||
modelsMap: sections,
|
||||
}));
|
||||
}
|
||||
|
||||
const fetchedMetadata = courseMetadataResult.status === 'fulfilled';
|
||||
const fetchedCourseHomeMetadata = courseHomeMetadataResult.status === 'fulfilled';
|
||||
const fetchedOutline = learningSequencesOutlineResult.status === 'fulfilled';
|
||||
|
||||
// Log errors for each request if needed. Outline failures may occur
|
||||
// even if the course metadata request is successful
|
||||
if (!fetchedOutline) {
|
||||
const { response } = learningSequencesOutlineResult.reason;
|
||||
if (response && response.status === 403) {
|
||||
// 403 responses are normal - they happen when the learner is logged out.
|
||||
// We'll redirect them in a moment to the outline tab by calling fetchCourseDenied() below.
|
||||
logInfo(learningSequencesOutlineResult.reason);
|
||||
} else {
|
||||
logError(learningSequencesOutlineResult.reason);
|
||||
}
|
||||
}
|
||||
if (!fetchedMetadata) {
|
||||
logError(courseMetadataResult.reason);
|
||||
}
|
||||
if (!fetchedCourseHomeMetadata) {
|
||||
logError(courseHomeMetadataResult.reason);
|
||||
}
|
||||
if (fetchedMetadata && fetchedCourseHomeMetadata) {
|
||||
if (courseHomeMetadataResult.value.courseAccess.hasAccess && fetchedOutline) {
|
||||
// User has access
|
||||
dispatch(fetchCourseSuccess({ courseId }));
|
||||
return;
|
||||
}
|
||||
// User either doesn't have access or only has partial access
|
||||
// (can't access course blocks)
|
||||
dispatch(fetchCourseDenied({ courseId }));
|
||||
return;
|
||||
}
|
||||
|
||||
// Definitely an error happening
|
||||
dispatch(fetchCourseFailure({ courseId }));
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
export function createNewCourseXBlock(body, callback, blockId) {
|
||||
return async (dispatch) => {
|
||||
dispatch(updateLoadingCourseXblockStatus({ status: RequestStatus.IN_PROGRESS }));
|
||||
|
||||
@@ -3,196 +3,7 @@ import { camelCaseObject } from '@edx/frontend-platform';
|
||||
import { NOTIFICATION_MESSAGES } from '../../constants';
|
||||
import { PUBLISH_TYPES } from '../constants';
|
||||
|
||||
export function getTimeOffsetMillis(headerDate, requestTime, responseTime) {
|
||||
// Time offset computation should move down into the HttpClient wrapper to maintain a global time correction reference
|
||||
// Requires 'Access-Control-Expose-Headers: Date' on the server response per https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS#access-control-expose-headers
|
||||
|
||||
let timeOffsetMillis = 0;
|
||||
if (headerDate !== undefined) {
|
||||
const headerTime = Date.parse(headerDate);
|
||||
const roundTripMillis = requestTime - responseTime;
|
||||
const localTime = responseTime - (roundTripMillis / 2); // Roughly compensate for transit time
|
||||
timeOffsetMillis = headerTime - localTime;
|
||||
}
|
||||
|
||||
return timeOffsetMillis;
|
||||
}
|
||||
|
||||
export function normalizeMetadata(metadata) {
|
||||
const requestTime = Date.now();
|
||||
const responseTime = requestTime;
|
||||
const { data, headers } = metadata;
|
||||
return {
|
||||
accessExpiration: camelCaseObject(data.access_expiration),
|
||||
canShowUpgradeSock: data.can_show_upgrade_sock,
|
||||
contentTypeGatingEnabled: data.content_type_gating_enabled,
|
||||
courseGoals: camelCaseObject(data.course_goals),
|
||||
id: data.id,
|
||||
title: data.name,
|
||||
offer: camelCaseObject(data.offer),
|
||||
enrollmentStart: data.enrollment_start,
|
||||
enrollmentEnd: data.enrollment_end,
|
||||
end: data.end,
|
||||
start: data.start,
|
||||
enrollmentMode: data.enrollment.mode,
|
||||
isEnrolled: data.enrollment.is_active,
|
||||
license: data.license,
|
||||
userTimezone: data.user_timezone,
|
||||
showCalculator: data.show_calculator,
|
||||
notes: camelCaseObject(data.notes),
|
||||
marketingUrl: data.marketing_url,
|
||||
celebrations: camelCaseObject(data.celebrations),
|
||||
userHasPassingGrade: data.user_has_passing_grade,
|
||||
courseExitPageIsActive: data.course_exit_page_is_active,
|
||||
certificateData: camelCaseObject(data.certificate_data),
|
||||
entranceExamData: camelCaseObject(data.entrance_exam_data),
|
||||
timeOffsetMillis: getTimeOffsetMillis(headers && headers.date, requestTime, responseTime),
|
||||
verifyIdentityUrl: data.verify_identity_url,
|
||||
verificationStatus: data.verification_status,
|
||||
linkedinAddToProfileUrl: data.linkedin_add_to_profile_url,
|
||||
relatedPrograms: camelCaseObject(data.related_programs),
|
||||
userNeedsIntegritySignature: data.user_needs_integrity_signature,
|
||||
canAccessProctoredExams: data.can_access_proctored_exams,
|
||||
learningAssistantEnabled: data.learning_assistant_enabled,
|
||||
};
|
||||
}
|
||||
|
||||
export const appendBrowserTimezoneToUrl = (url) => {
|
||||
const browserTimezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
|
||||
const urlObject = new URL(url);
|
||||
if (browserTimezone) {
|
||||
urlObject.searchParams.append('browser_timezone', browserTimezone);
|
||||
}
|
||||
return urlObject.href;
|
||||
};
|
||||
|
||||
export function normalizeSequenceMetadata(sequence) {
|
||||
return {
|
||||
sequence: {
|
||||
id: sequence.item_id,
|
||||
blockType: sequence.tag,
|
||||
unitIds: sequence.items.map(unit => unit.id),
|
||||
bannerText: sequence.banner_text,
|
||||
format: sequence.format,
|
||||
title: sequence.display_name,
|
||||
/*
|
||||
Example structure of gated_content when prerequisites exist:
|
||||
{
|
||||
prereq_id: 'id of the prereq section',
|
||||
prereq_url: 'unused by this frontend',
|
||||
prereq_section_name: 'Name of the prerequisite section',
|
||||
gated: true,
|
||||
gated_section_name: 'Name of this gated section',
|
||||
*/
|
||||
gatedContent: camelCaseObject(sequence.gated_content),
|
||||
isTimeLimited: sequence.is_time_limited,
|
||||
isProctored: sequence.is_proctored,
|
||||
isHiddenAfterDue: sequence.is_hidden_after_due,
|
||||
// Position comes back from the server 1-indexed. Adjust here.
|
||||
activeUnitIndex: sequence.position ? sequence.position - 1 : 0,
|
||||
saveUnitPosition: sequence.save_position,
|
||||
showCompletion: sequence.show_completion,
|
||||
allowProctoringOptOut: sequence.allow_proctoring_opt_out,
|
||||
},
|
||||
units: sequence.items.map(unit => ({
|
||||
id: unit.id,
|
||||
sequenceId: sequence.item_id,
|
||||
bookmarked: unit.bookmarked,
|
||||
complete: unit.complete,
|
||||
title: unit.page_title,
|
||||
contentType: unit.type,
|
||||
graded: unit.graded,
|
||||
containsContentTypeGatedContent: unit.contains_content_type_gated_content,
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
export function normalizeLearningSequencesData(learningSequencesData) {
|
||||
const models = {
|
||||
courses: {},
|
||||
sections: {},
|
||||
sequences: {},
|
||||
};
|
||||
|
||||
const now = new Date();
|
||||
function isReleased(block) {
|
||||
// We check whether the backend marks this as accessible because staff users are granted access anyway.
|
||||
// Note that sections don't have the `accessible` field and will just be checking `effective_start`.
|
||||
return block.accessible || !block.effective_start || now >= Date.parse(block.effective_start);
|
||||
}
|
||||
|
||||
// Sequences
|
||||
Object.entries(learningSequencesData.outline.sequences).forEach(([seqId, sequence]) => {
|
||||
if (!isReleased(sequence)) {
|
||||
return; // Don't let the learner see unreleased sequences
|
||||
}
|
||||
|
||||
models.sequences[seqId] = {
|
||||
id: seqId,
|
||||
title: sequence.title,
|
||||
};
|
||||
});
|
||||
|
||||
// Sections
|
||||
learningSequencesData.outline.sections.forEach(section => {
|
||||
// Filter out any ignored sequences (e.g. unreleased sequences)
|
||||
const availableSequenceIds = section.sequence_ids.filter(seqId => seqId in models.sequences);
|
||||
|
||||
// If we are unreleased and already stripped out all our children, just don't show us at all.
|
||||
// (We check both release date and children because children will exist for an unreleased section even for staff,
|
||||
// so we still want to show this section.)
|
||||
if (!isReleased(section) && !availableSequenceIds.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
models.sections[section.id] = {
|
||||
id: section.id,
|
||||
title: section.title,
|
||||
sequenceIds: availableSequenceIds,
|
||||
courseId: learningSequencesData.course_key,
|
||||
};
|
||||
|
||||
// Add back-references to this section for all child sequences.
|
||||
availableSequenceIds.forEach(childSeqId => {
|
||||
models.sequences[childSeqId].sectionId = section.id;
|
||||
});
|
||||
});
|
||||
|
||||
// Course
|
||||
models.courses[learningSequencesData.course_key] = {
|
||||
id: learningSequencesData.course_key,
|
||||
title: learningSequencesData.title,
|
||||
sectionIds: Object.entries(models.sections).map(([sectionId]) => sectionId),
|
||||
|
||||
// Scan through all the sequences and look for ones that aren't released yet.
|
||||
hasScheduledContent: Object.values(learningSequencesData.outline.sequences).some(seq => !isReleased(seq)),
|
||||
};
|
||||
|
||||
return models;
|
||||
}
|
||||
|
||||
/**
|
||||
* Tweak the metadata for consistency
|
||||
* @param metadata the data to normalize
|
||||
* @param rootSlug either 'courseware' or 'outline' depending on the context
|
||||
* @returns {Object} The normalized metadata
|
||||
*/
|
||||
export function normalizeCourseHomeCourseMetadata(metadata, rootSlug) {
|
||||
const data = camelCaseObject(metadata);
|
||||
return {
|
||||
...data,
|
||||
tabs: data.tabs.map(tab => ({
|
||||
// The API uses "courseware" as a slug for both courseware and the outline tab.
|
||||
// If needed, we switch it to "outline" here for
|
||||
// use within the MFE to differentiate between course home and courseware.
|
||||
slug: tab.tabId === 'courseware' ? rootSlug : tab.tabId,
|
||||
title: tab.title,
|
||||
url: tab.url,
|
||||
})),
|
||||
isMasquerading: data.originalUserIsStaff && !data.isStaff,
|
||||
};
|
||||
}
|
||||
|
||||
// eslint-disable-next-line import/prefer-default-export
|
||||
export function normalizeCourseSectionVerticalData(metadata) {
|
||||
const data = camelCaseObject(metadata);
|
||||
return {
|
||||
|
||||
@@ -7,7 +7,6 @@ import {
|
||||
createNewCourseXBlock,
|
||||
fetchCourseUnitQuery,
|
||||
editCourseItemQuery,
|
||||
fetchCourse,
|
||||
fetchCourseSectionVerticalData,
|
||||
fetchCourseVerticalChildrenData,
|
||||
deleteUnitItemQuery,
|
||||
@@ -19,6 +18,7 @@ import {
|
||||
getCourseUnitData,
|
||||
getLoadingStatus,
|
||||
getSavingStatus,
|
||||
getSequenceStatus,
|
||||
} from './data/selectors';
|
||||
import { changeEditTitleFormOpen, updateQueryPendingStatus } from './data/slice';
|
||||
|
||||
@@ -31,6 +31,7 @@ export const useCourseUnit = ({ courseId, blockId }) => {
|
||||
const courseUnit = useSelector(getCourseUnitData);
|
||||
const savingStatus = useSelector(getSavingStatus);
|
||||
const loadingStatus = useSelector(getLoadingStatus);
|
||||
const sequenceStatus = useSelector(getSequenceStatus);
|
||||
const { draftPreviewLink, publishedPreviewLink } = useSelector(getCourseSectionVertical);
|
||||
const courseVerticalChildren = useSelector(getCourseVerticalChildren);
|
||||
const navigate = useNavigate();
|
||||
@@ -87,7 +88,7 @@ export const useCourseUnit = ({ courseId, blockId }) => {
|
||||
|
||||
useEffect(() => {
|
||||
if (savingStatus === RequestStatus.SUCCESSFUL) {
|
||||
dispatch(updateQueryPendingStatus(false));
|
||||
dispatch(updateQueryPendingStatus(true));
|
||||
} else if (savingStatus === RequestStatus.FAILED && !hasInternetConnectionError) {
|
||||
toggleErrorAlert(true);
|
||||
}
|
||||
@@ -97,7 +98,6 @@ export const useCourseUnit = ({ courseId, blockId }) => {
|
||||
dispatch(fetchCourseUnitQuery(blockId));
|
||||
dispatch(fetchCourseSectionVerticalData(blockId, sequenceId));
|
||||
dispatch(fetchCourseVerticalChildrenData(blockId));
|
||||
dispatch(fetchCourse(courseId));
|
||||
|
||||
handleNavigate(sequenceId);
|
||||
}, [courseId, blockId, sequenceId]);
|
||||
@@ -106,6 +106,7 @@ export const useCourseUnit = ({ courseId, blockId }) => {
|
||||
sequenceId,
|
||||
courseUnit,
|
||||
unitTitle,
|
||||
sequenceStatus,
|
||||
savingStatus,
|
||||
isQueryPending,
|
||||
isErrorAlert,
|
||||
|
||||
Reference in New Issue
Block a user