feat: Unit creation button logic and refactoring
This commit is contained in:
committed by
Adolfo R. Brandes
parent
90fb3d8edc
commit
7fcc501d2e
@@ -4,10 +4,10 @@ import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
|
||||
|
||||
import {
|
||||
normalizeLearningSequencesData,
|
||||
normalizeSequenceMetadata,
|
||||
normalizeMetadata,
|
||||
normalizeCourseHomeCourseMetadata,
|
||||
appendBrowserTimezoneToUrl,
|
||||
normalizeCourseSectionVerticalData,
|
||||
} from './utils';
|
||||
|
||||
const getStudioBaseUrl = () => getConfig().STUDIO_BASE_URL;
|
||||
@@ -17,7 +17,6 @@ export const getCourseUnitApiUrl = (itemId) => `${getStudioBaseUrl()}/xblock/con
|
||||
export const postXBlockBaseApiUrl = () => `${getStudioBaseUrl()}/xblock/`;
|
||||
export const getXBlockBaseApiUrl = (itemId) => `${getStudioBaseUrl()}/xblock/${itemId}`;
|
||||
export const getCourseSectionVerticalApiUrl = (itemId) => `${getStudioBaseUrl()}/api/contentstore/v1/container_handler/${itemId}`;
|
||||
export const getSequenceMetadataApiUrl = (sequenceId) => `${getLmsBaseUrl()}/api/courseware/sequence/${sequenceId}`;
|
||||
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}`;
|
||||
@@ -51,18 +50,6 @@ export async function editUnitDisplayName(unitId, displayName) {
|
||||
return data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get sequence metadata for a given sequence ID.
|
||||
* @param {string} sequenceId - The ID of the sequence for which metadata is requested.
|
||||
* @returns {Promise<Object>} - A Promise that resolves to the normalized sequence metadata.
|
||||
*/
|
||||
export async function getSequenceMetadata(sequenceId) {
|
||||
const { data } = await getAuthenticatedHttpClient()
|
||||
.get(getSequenceMetadataApiUrl(sequenceId), {});
|
||||
|
||||
return normalizeSequenceMetadata(data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an object containing course section vertical data.
|
||||
* @param {string} unitId
|
||||
@@ -72,7 +59,7 @@ export async function getCourseSectionVerticalData(unitId) {
|
||||
const { data } = await getAuthenticatedHttpClient()
|
||||
.get(getCourseSectionVerticalApiUrl(unitId));
|
||||
|
||||
return camelCaseObject(data);
|
||||
return normalizeCourseSectionVerticalData(data);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -114,11 +101,14 @@ export async function getCourseHomeCourseMetadata(courseId, rootSlug) {
|
||||
return normalizeCourseHomeCourseMetadata(data, rootSlug);
|
||||
}
|
||||
|
||||
export async function createCourseXblock({ type, category, parentLocator }) {
|
||||
export async function createCourseXblock({
|
||||
type, category, parentLocator, displayName,
|
||||
}) {
|
||||
const body = {
|
||||
type,
|
||||
category: category || type,
|
||||
parent_locator: parentLocator,
|
||||
display_name: displayName,
|
||||
};
|
||||
const { data } = await getAuthenticatedHttpClient()
|
||||
.post(postXBlockBaseApiUrl(), body);
|
||||
|
||||
@@ -3,20 +3,18 @@ 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 getCourseSectionVertical = (state) => state.courseUnit.courseSectionVertical;
|
||||
|
||||
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 sequenceIdsSelector = createSelector(
|
||||
[getCourseStatus, getCoursewareMeta, getSections, getCourseId],
|
||||
(courseStatus, coursewareMeta, sections, courseId) => {
|
||||
|
||||
@@ -7,6 +7,8 @@ const slice = createSlice({
|
||||
name: 'courseUnit',
|
||||
initialState: {
|
||||
savingStatus: '',
|
||||
isQueryPending: false,
|
||||
isEditTitleFormOpen: false,
|
||||
loadingStatus: {
|
||||
fetchUnitLoadingStatus: RequestStatus.IN_PROGRESS,
|
||||
courseSectionVerticalLoadingStatus: RequestStatus.IN_PROGRESS,
|
||||
@@ -24,6 +26,12 @@ const slice = createSlice({
|
||||
fetchUnitLoadingStatus: payload.status,
|
||||
};
|
||||
},
|
||||
updateQueryPendingStatus: (state, { payload }) => {
|
||||
state.isQueryPending = payload;
|
||||
},
|
||||
changeEditTitleFormOpen: (state, { payload }) => {
|
||||
state.isEditTitleFormOpen = payload;
|
||||
},
|
||||
updateSavingStatus: (state, { payload }) => {
|
||||
state.savingStatus = payload.status;
|
||||
},
|
||||
@@ -73,6 +81,12 @@ const slice = createSlice({
|
||||
createUnitXblockLoadingStatus: payload.status,
|
||||
};
|
||||
},
|
||||
addNewUnitStatus: (state, { payload }) => {
|
||||
state.loadingStatus = {
|
||||
...state.loadingStatus,
|
||||
fetchUnitLoadingStatus: payload.status,
|
||||
};
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -90,6 +104,8 @@ export const {
|
||||
fetchCourseDenied,
|
||||
fetchCourseSectionVerticalDataSuccess,
|
||||
updateLoadingCourseSectionVerticalDataStatus,
|
||||
changeEditTitleFormOpen,
|
||||
updateQueryPendingStatus,
|
||||
updateLoadingCourseXblockStatus,
|
||||
} = slice.actions;
|
||||
|
||||
|
||||
@@ -12,7 +12,6 @@ import {
|
||||
import {
|
||||
getCourseUnitData,
|
||||
editUnitDisplayName,
|
||||
getSequenceMetadata,
|
||||
getCourseMetadata,
|
||||
getLearningSequencesOutline,
|
||||
getCourseHomeCourseMetadata,
|
||||
@@ -51,23 +50,34 @@ export function fetchCourseUnitQuery(courseId) {
|
||||
};
|
||||
}
|
||||
|
||||
export function fetchCourseSectionVerticalData(courseId) {
|
||||
export function fetchCourseSectionVerticalData(courseId, sequenceId) {
|
||||
return async (dispatch) => {
|
||||
dispatch(updateLoadingCourseSectionVerticalDataStatus({ status: RequestStatus.IN_PROGRESS }));
|
||||
dispatch(fetchSequenceRequest({ sequenceId }));
|
||||
|
||||
try {
|
||||
const courseSectionVerticalData = await getCourseSectionVerticalData(courseId);
|
||||
dispatch(fetchCourseSectionVerticalDataSuccess(courseSectionVerticalData));
|
||||
dispatch(updateLoadingCourseSectionVerticalDataStatus({ status: RequestStatus.SUCCESSFUL }));
|
||||
dispatch(updateModel({
|
||||
modelType: 'sequences',
|
||||
model: courseSectionVerticalData.sequence,
|
||||
}));
|
||||
dispatch(updateModels({
|
||||
modelType: 'units',
|
||||
models: courseSectionVerticalData.units,
|
||||
}));
|
||||
dispatch(fetchSequenceSuccess({ sequenceId }));
|
||||
return true;
|
||||
} catch (error) {
|
||||
dispatch(updateLoadingCourseSectionVerticalDataStatus({ status: RequestStatus.FAILED }));
|
||||
dispatch(fetchSequenceFailure({ sequenceId }));
|
||||
return false;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export function editCourseItemQuery(itemId, displayName) {
|
||||
export function editCourseItemQuery(itemId, displayName, sequenceId) {
|
||||
return async (dispatch) => {
|
||||
dispatch(updateSavingStatus({ status: RequestStatus.PENDING }));
|
||||
dispatch(showProcessingNotification(NOTIFICATION_MESSAGES.saving));
|
||||
@@ -76,6 +86,18 @@ export function editCourseItemQuery(itemId, displayName) {
|
||||
await editUnitDisplayName(itemId, displayName).then(async (result) => {
|
||||
if (result) {
|
||||
const courseUnit = await getCourseUnitData(itemId);
|
||||
const courseSectionVerticalData = await getCourseSectionVerticalData(itemId);
|
||||
dispatch(fetchCourseSectionVerticalDataSuccess(courseSectionVerticalData));
|
||||
dispatch(updateLoadingCourseSectionVerticalDataStatus({ status: RequestStatus.SUCCESSFUL }));
|
||||
dispatch(updateModel({
|
||||
modelType: 'sequences',
|
||||
model: courseSectionVerticalData.sequence,
|
||||
}));
|
||||
dispatch(updateModels({
|
||||
modelType: 'units',
|
||||
models: courseSectionVerticalData.units,
|
||||
}));
|
||||
dispatch(fetchSequenceSuccess({ sequenceId }));
|
||||
dispatch(fetchCourseItemSuccess(courseUnit));
|
||||
dispatch(hideProcessingNotification());
|
||||
dispatch(updateSavingStatus({ status: RequestStatus.SUCCESSFUL }));
|
||||
@@ -88,45 +110,6 @@ export function editCourseItemQuery(itemId, displayName) {
|
||||
};
|
||||
}
|
||||
|
||||
export function fetchSequence(sequenceId) {
|
||||
return async (dispatch) => {
|
||||
dispatch(fetchSequenceRequest({ sequenceId }));
|
||||
try {
|
||||
const { sequence, units } = await getSequenceMetadata(sequenceId);
|
||||
|
||||
if (sequence.blockType !== 'sequential') {
|
||||
// Some other block types (particularly 'chapter') can be returned
|
||||
// by this API. We want to error in that case, since downstream
|
||||
// courseware code is written to render Sequences of Units.
|
||||
logError(
|
||||
`Requested sequence '${sequenceId}' `
|
||||
+ `has block type '${sequence.blockType}'; expected block type 'sequential'.`,
|
||||
);
|
||||
dispatch(fetchSequenceFailure({ sequenceId }));
|
||||
} else {
|
||||
dispatch(updateModel({
|
||||
modelType: 'sequences',
|
||||
model: sequence,
|
||||
}));
|
||||
dispatch(updateModels({
|
||||
modelType: 'units',
|
||||
models: units,
|
||||
}));
|
||||
dispatch(fetchSequenceSuccess({ sequenceId }));
|
||||
}
|
||||
} catch (error) {
|
||||
// Some errors are expected - for example, CoursewareContainer may request sequence metadata for a unit and rely
|
||||
// on the request failing to notice that it actually does have a unit (mostly so it doesn't have to know anything
|
||||
// about the opaque key structure). In such cases, the backend gives us a 422.
|
||||
const sequenceMightBeUnit = error?.response?.status === 422;
|
||||
if (!sequenceMightBeUnit) {
|
||||
logError(error);
|
||||
}
|
||||
dispatch(fetchSequenceFailure({ sequenceId, sequenceMightBeUnit }));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export function fetchCourse(courseId) {
|
||||
return async (dispatch) => {
|
||||
dispatch(fetchCourseRequest({ courseId }));
|
||||
@@ -156,9 +139,7 @@ export function fetchCourse(courseId) {
|
||||
}
|
||||
|
||||
if (learningSequencesOutlineResult.status === 'fulfilled') {
|
||||
const {
|
||||
courses, sections, sequences,
|
||||
} = learningSequencesOutlineResult.value;
|
||||
const { courses, sections } = learningSequencesOutlineResult.value;
|
||||
|
||||
// This updates the course with a sectionIds array from the Learning Sequence data.
|
||||
dispatch(updateModelsMap({
|
||||
@@ -169,11 +150,6 @@ export function fetchCourse(courseId) {
|
||||
modelType: 'sections',
|
||||
modelsMap: sections,
|
||||
}));
|
||||
// We update for sequences because the sequence metadata may have come back first.
|
||||
dispatch(updateModelsMap({
|
||||
modelType: 'sequences',
|
||||
modelsMap: sequences,
|
||||
}));
|
||||
}
|
||||
|
||||
const fetchedMetadata = courseMetadataResult.status === 'fulfilled';
|
||||
@@ -225,6 +201,10 @@ export function createNewCourseXblock(body, callback) {
|
||||
try {
|
||||
await createCourseXblock(body).then(async (result) => {
|
||||
if (result) {
|
||||
if (body.category === 'vertical') {
|
||||
const courseSectionVerticalData = await getCourseSectionVerticalData(result.locator);
|
||||
dispatch(fetchCourseSectionVerticalDataSuccess(courseSectionVerticalData));
|
||||
}
|
||||
// ToDo: implement fetching (update) xblocks after success creating
|
||||
dispatch(hideProcessingNotification());
|
||||
dispatch(updateLoadingCourseXblockStatus({ status: RequestStatus.SUCCESSFUL }));
|
||||
|
||||
@@ -189,3 +189,25 @@ export function normalizeCourseHomeCourseMetadata(metadata, rootSlug) {
|
||||
isMasquerading: data.originalUserIsStaff && !data.isStaff,
|
||||
};
|
||||
}
|
||||
|
||||
export function normalizeCourseSectionVerticalData(metadata) {
|
||||
const data = camelCaseObject(metadata);
|
||||
return {
|
||||
...data,
|
||||
sequence: {
|
||||
id: data.subsectionLocation,
|
||||
title: data.xblock.displayName,
|
||||
unitIds: data.xblockInfo.ancestorInfo.ancestors[0].childInfo.children.map((item) => item.id),
|
||||
},
|
||||
units: data.xblockInfo.ancestorInfo.ancestors[0].childInfo.children.map((unit) => ({
|
||||
id: unit.id,
|
||||
sequenceId: data.subsectionLocation,
|
||||
bookmarked: unit.bookmarked,
|
||||
complete: unit.complete,
|
||||
title: unit.displayName,
|
||||
contentType: unit.xblockType,
|
||||
graded: unit.graded,
|
||||
containsContentTypeGatedContent: unit.contains_content_type_gated_content,
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user