Skip to content
Merged
Show file tree
Hide file tree
Changes from 13 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
5 changes: 4 additions & 1 deletion plugins/course-apps/proctoring/Settings.test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,8 @@ describe('ProctoredExamSettings', () => {
provider: null,
});

axiosMock.onGet(/course_index/).reply(200, { sections: [] });

axiosMock.onGet(
StudioApiService.getProctoredExamSettingsUrl(defaultProps.courseId),
).reply(200, {
Expand Down Expand Up @@ -466,7 +468,8 @@ describe('ProctoredExamSettings', () => {
// (1) for studio settings
// (2) waffle flags
// (3) for course details
expect(axiosMock.history.get.length).toBe(3);
// (4) for course outline index (from CourseAuthoringProvider)
expect(axiosMock.history.get.length).toBe(4);
expect(axiosMock.history.get[0].url.includes('proctored_exam_settings')).toEqual(true);
});

Expand Down
272 changes: 268 additions & 4 deletions src/CourseAuthoringContext.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,23 @@
import { getConfig } from '@edx/frontend-platform';
import {
createContext, useContext, useMemo, useState,
createContext, useCallback, useContext, useEffect, useMemo, useState,
} from 'react';
import { getAuthenticatedUser } from '@edx/frontend-platform/auth';
import { useCreateCourseBlock } from '@src/course-outline/data/apiHooks';
import { useSelector } from 'react-redux';
import { useCreateCourseBlock, useDeleteCourseItem, useDuplicateItem } from '@src/course-outline/data/apiHooks';
import { useDispatch, useSelector } from 'react-redux';
import { useNavigate } from 'react-router';
import { getOutlineIndexData } from '@src/course-outline/data/selectors';
import { getOutlineIndexData, getSectionsList } from '@src/course-outline/data/selectors';
import { useToggleWithValue } from '@src/hooks';
import { SelectionState, type UnitXBlock, type XBlock } from '@src/data/types';
import { CourseDetailsData } from './data/api';
import { useCourseDetails, useWaffleFlags } from './data/apiHooks';
import { RequestStatusType } from './data/constants';
import { arrayMove } from '@dnd-kit/sortable';
import { fetchCourseOutlineIndexQuery, setSectionOrderListQuery, setSubsectionOrderListQuery, setUnitOrderListQuery } from './course-outline/data/thunk';
import { useToggle } from '@openedx/paragon';
import { getBlockType } from './generic/key-utils';
import { COURSE_BLOCK_NAMES } from './constants';
import { deleteSection, deleteSubsection, deleteUnit } from './course-outline/data/slice';

type ModalState = {
value?: XBlock | UnitXBlock;
Expand Down Expand Up @@ -40,6 +46,23 @@ export type CourseAuthoringContextData = {
closePublishModal: () => void;
currentSelection?: SelectionState;
setCurrentSelection: React.Dispatch<React.SetStateAction<SelectionState | undefined>>;
sections: XBlock[];
restoreSectionList: () => void;
setSections: React.Dispatch<React.SetStateAction<XBlock[]>>;
isDuplicatingItem: boolean;
isDeleteModalOpen: boolean;
openDeleteModal: () => void;
closeDeleteModal: () => void;
getHandleDeleteItemSubmit: (callback: () => void) => () => Promise<void>;
handleDuplicateSectionSubmit: () => void;
handleDuplicateSubsectionSubmit: () => void;
handleDuplicateUnitSubmit: () => void;
handleSectionDragAndDrop: (sectionListIds: string[]) => void;
handleSubsectionDragAndDrop: (sectionId: string, prevSectionId: string, subsectionListIds: string[]) => void;
handleUnitDragAndDrop: (sectionId: string, prevSectionId: string, subsectionId: string, unitListIds: string[]) => void;
updateSectionOrderByIndex: (currentIndex: number, newIndex: number) => void;
updateSubsectionOrderByIndex: (section: XBlock, moveDetails: any) => void;
updateUnitOrderByIndex: (section: XBlock, moveDetails: any) => void;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This context is getting very big, and many of these new additions do not seem relevant to most course pages like advanced settings, files & uploads, unit content, sync updates, etc.

What do you think about moving some of these to a separate CourseOutlineContext that's only for the course outline page?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done. I refactored the code in 5b332b2

};

/**
Expand All @@ -61,6 +84,7 @@ export const CourseAuthoringProvider = ({
courseId,
}: CourseAuthoringProviderProps) => {
const navigate = useNavigate();
const dispatch = useDispatch();
const waffleFlags = useWaffleFlags();
const { data: courseDetails, status: courseDetailStatus } = useCourseDetails(courseId);
const canChangeProviders = getAuthenticatedUser().administrator || new Date(courseDetails?.start ?? 0) > new Date();
Expand All @@ -78,6 +102,23 @@ export const CourseAuthoringProvider = ({
openPublishModal,
closePublishModal,
] = useToggleWithValue<ModalState>();
const sectionsList = useSelector(getSectionsList);
const [sections, setSections] = useState<XBlock[]>(sectionsList);
const [isDeleteModalOpen, openDeleteModal, closeDeleteModal] = useToggle(false);


const restoreSectionList = () => {
setSections(() => [...sectionsList]);
};

useEffect(() => {
dispatch(fetchCourseOutlineIndexQuery(courseId));
}, [courseId]);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this required? We have the same in hooks.jsx

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, it makes more sense to put it there, since that's where the sections are being saved. I've removed the duplicate.


useEffect(() => {
setSections(sectionsList);
}, [sectionsList]);

/**
* This will hold the state of current item that is being operated on,
* For example:
Expand Down Expand Up @@ -113,6 +154,196 @@ export const CourseAuthoringProvider = ({
const handleAddAndOpenUnit = useCreateCourseBlock(courseId, openUnitPage);
const handleAddBlock = useCreateCourseBlock(courseId);

const {
mutate: duplicateItem,
isPending: isDuplicatingItem,
} = useDuplicateItem(courseId);
const handleDuplicateSectionSubmit = () => {
if (currentSelection && currentSelection.currentId) {
duplicateItem({
itemId: currentSelection.currentId,
parentId: courseStructure.id,
sectionId: currentSelection.sectionId,
subsectionId: currentSelection.subsectionId,
});
}
};

const handleDuplicateSubsectionSubmit = () => {
if (currentSelection && currentSelection.currentId && currentSelection.sectionId) {
duplicateItem({
itemId: currentSelection.currentId,
parentId: currentSelection.sectionId,
sectionId: currentSelection.sectionId,
subsectionId: currentSelection.subsectionId,
});
}
};

const handleDuplicateUnitSubmit = () => {
if (currentSelection && currentSelection.currentId && currentSelection.subsectionId) {
duplicateItem({
itemId: currentSelection?.currentId,
parentId: currentSelection?.subsectionId,
sectionId: currentSelection?.sectionId,
subsectionId: currentSelection?.subsectionId,
});
}
};
Comment thread
ChrisChV marked this conversation as resolved.
Outdated

const handleSectionDragAndDrop = (
sectionListIds: string[],
) => {
dispatch(setSectionOrderListQuery(
courseId,
sectionListIds,
restoreSectionList,
));
};

const handleSubsectionDragAndDrop = (
sectionId: string,
prevSectionId: string,
subsectionListIds: string[],
) => {
dispatch(setSubsectionOrderListQuery(
sectionId,
prevSectionId,
subsectionListIds,
restoreSectionList,
));
};

const handleUnitDragAndDrop = (
sectionId: string,
prevSectionId: string,
subsectionId: string,
unitListIds: string[],
) => {
dispatch(setUnitOrderListQuery(
sectionId,
subsectionId,
prevSectionId,
unitListIds,
restoreSectionList,
));
};

/**
* Uses details from move information and moves unit
*/
const updateUnitOrderByIndex = (section: XBlock, moveDetails) => {
const { fn, args, sectionId, subsectionId } = moveDetails;
if (!args) {
return;
}
const [sectionsCopy, newUnits] = fn(...args);
if (newUnits && subsectionId) {
setSections(sectionsCopy);
handleUnitDragAndDrop(
sectionId,
section.id,
subsectionId,
newUnits.map((unit) => unit.id),
);
}
};

/**
* Move section to new index
*/
const updateSectionOrderByIndex = (currentIndex: number, newIndex: number) => {
if (currentIndex === newIndex) {
return;
}
setSections((prevSections) => {
const newSections = arrayMove(prevSections, currentIndex, newIndex);
handleSectionDragAndDrop(newSections.map(section => section.id));
return newSections;
});
};

/**
* Uses details from move information and moves subsection
*/
const updateSubsectionOrderByIndex = (section: XBlock, moveDetails) => {
const { fn, args, sectionId } = moveDetails;
if (!args) {
return;
}
const [sectionsCopy, newSubsections] = fn(...args);
if (newSubsections && sectionId) {
setSections(sectionsCopy);
handleSubsectionDragAndDrop(
sectionId,
section.id,
newSubsections.map(subsection => subsection.id),
);
}
};

const deleteMutation = useDeleteCourseItem();

const getHandleDeleteItemSubmit = useCallback((callback: () => void) => {
return async () => {
// istanbul ignore if
if (!currentSelection) {
return;
}
const category = getBlockType(currentSelection.currentId);
switch (category) {
case COURSE_BLOCK_NAMES.chapter.id:
await deleteMutation.mutateAsync(
{ itemId: currentSelection.currentId },
{
onSettled: () => dispatch(deleteSection({ itemId: currentSelection.currentId })),
},
);
break;
case COURSE_BLOCK_NAMES.sequential.id:
await deleteMutation.mutateAsync(
{ itemId: currentSelection.currentId, sectionId: currentSelection.sectionId },
{
onSettled: () => dispatch(deleteSubsection({
itemId: currentSelection.currentId,
sectionId: currentSelection.sectionId,
})),
},
);
break;
case COURSE_BLOCK_NAMES.vertical.id:
await deleteMutation.mutateAsync(
{
itemId: currentSelection.currentId,
subsectionId: currentSelection.subsectionId,
sectionId: currentSelection.sectionId,
},
{
onSettled: () => dispatch(deleteUnit({
itemId: currentSelection.currentId,
subsectionId: currentSelection.subsectionId,
sectionId: currentSelection.sectionId,
})),
},
);
break;
default:
// istanbul ignore next
throw new Error(`Unrecognized category ${category}`);
}
closeDeleteModal();
callback();
};
}, [
deleteMutation,
closeDeleteModal,
currentSelection,
dispatch,
deleteSection,
deleteUnit,
deleteSubsection,
]);

const context = useMemo<CourseAuthoringContextData>(() => ({
courseId,
courseUsageKey,
Expand All @@ -133,6 +364,23 @@ export const CourseAuthoringProvider = ({
closePublishModal,
currentSelection,
setCurrentSelection,
sections,
restoreSectionList,
setSections,
isDuplicatingItem,
isDeleteModalOpen,
openDeleteModal,
closeDeleteModal,
getHandleDeleteItemSubmit,
handleDuplicateSectionSubmit,
handleDuplicateSubsectionSubmit,
handleDuplicateUnitSubmit,
handleSectionDragAndDrop,
handleSubsectionDragAndDrop,
handleUnitDragAndDrop,
updateSectionOrderByIndex,
updateSubsectionOrderByIndex,
updateUnitOrderByIndex,
}), [
courseId,
courseUsageKey,
Expand All @@ -153,6 +401,22 @@ export const CourseAuthoringProvider = ({
closePublishModal,
currentSelection,
setCurrentSelection,
sections,
restoreSectionList,
setSections,
isDuplicatingItem,
isDeleteModalOpen,
openDeleteModal,
closeDeleteModal,
getHandleDeleteItemSubmit,
handleDuplicateSectionSubmit,
handleDuplicateSubsectionSubmit,
handleSectionDragAndDrop,
handleSubsectionDragAndDrop,
handleUnitDragAndDrop,
updateSectionOrderByIndex,
updateSubsectionOrderByIndex,
updateUnitOrderByIndex,
]);

return (
Expand Down
2 changes: 1 addition & 1 deletion src/content-tags-drawer/data/apiHooks.test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ describe('useTaxonomyTagsData', () => {
// Assert that useQueries was called with the correct arguments
expect(useQueries).toHaveBeenCalledWith({
queries: [
{ queryKey: ['taxonomyTags', taxonomyId, null, 1, ''], queryFn: expect.any(Function), staleTime: Infinity },
{ queryKey: ['contentTags', 'taxonomyTags', taxonomyId, null, 1, ''], queryFn: expect.any(Function), staleTime: Infinity },
],
});

Expand Down
Loading
Loading