feat: Unit creation button logic and refactoring

This commit is contained in:
Peter Kulko
2024-01-15 14:34:42 +02:00
committed by Adolfo R. Brandes
parent 90fb3d8edc
commit 7fcc501d2e
20 changed files with 393 additions and 138 deletions

View File

@@ -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);

View File

@@ -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) => {

View File

@@ -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;

View File

@@ -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 }));

View File

@@ -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,
})),
};
}