diff --git a/frontend/src/components/module-editor/LessonItem.tsx b/frontend/src/components/module-editor/LessonItem.tsx
index acd57734..047fe434 100644
--- a/frontend/src/components/module-editor/LessonItem.tsx
+++ b/frontend/src/components/module-editor/LessonItem.tsx
@@ -1,76 +1,88 @@
import React, { useState } from "react";
import { Button, IconButton, Flex } from "@chakra-ui/react";
import { EditIcon, DeleteIcon } from "@chakra-ui/icons";
+import { Draggable } from "react-beautiful-dnd";
import { ReactComponent as DragHandleIconSvg } from "../../assets/DragHandle.svg";
interface OptionsProps {
+ id: string;
text: string;
isFocused: boolean;
setFocus: () => void;
+ index: number;
}
+/* eslint-disable react/jsx-props-no-spreading */
+
const LessonItem = ({
- text = "",
+ id,
+ text,
isFocused,
setFocus,
+ index,
}: OptionsProps): React.ReactElement => {
const [isHovered, setIsHovered] = useState(false);
-
return (
- <>
-
- >
+
+ )}
+
);
};
diff --git a/frontend/src/components/module-editor/SideBar.tsx b/frontend/src/components/module-editor/SideBar.tsx
index db574503..e0b68b9b 100644
--- a/frontend/src/components/module-editor/SideBar.tsx
+++ b/frontend/src/components/module-editor/SideBar.tsx
@@ -69,6 +69,7 @@ const Sidebar = (): React.ReactElement => {
refetchQueries: [
{
query: GET_COURSE,
+ variables: { id: courseID }
},
],
},
@@ -124,38 +125,45 @@ const Sidebar = (): React.ReactElement => {
oldID: string,
changedLesson: LessonRequest,
) => {
+ console.log(oldID);
const { data } = await createLesson({
variables: { lesson: changedLesson },
});
if (!data) throw Error("Failed to create new lesson");
- const lesson = data.createLesson;
- const { id: newID } = lesson;
+ // const lesson = data.createLesson;
+ // const { id: newID } = lesson;
// Update lesson ID in state with response
- dispatch({ type: "update-lesson-id", value: { oldID, newID } });
+ dispatch({ type: "update-lesson-id", value: { oldID, newID: 'test' } });
};
const saveChanges = async (changeObj: EditorChangeStatuses) => {
+ console.log(changeObj);
Object.entries(changeObj).forEach(async ([doc_id, action]) => {
- const changedLesson = formatLessonRequest(state.lessons[doc_id]);
+
switch (action) {
case "CREATE":
// Await required so we can get a new ID
- await createNewLesson(doc_id, changedLesson);
+ await createNewLesson(doc_id, formatLessonRequest(state.lessons[doc_id]));
break;
case "UPDATE":
- updateLesson({ variables: { id: doc_id, lesson: changedLesson } });
+ updateLesson({ variables: { id: doc_id, lesson: formatLessonRequest(state.lessons[doc_id]) } });
break;
case "DELETE":
deleteLesson({ variables: { id: doc_id } });
break;
+ case "COURSE-UPDATE":
+ // Nothing happens because the updateCourse mutation is already called at the end anyway
+ break;
// Make compiler happy
default:
break;
- }
- updateCourse({
- variables: { id: changedLesson.course, course: state.course },
- });
+ }
});
+ return;
+ updateCourse({
+ variables: { id: courseID, course: state.course },
+ });
+
state.hasChanged = {};
};
diff --git a/frontend/src/components/module-editor/SideBarModuleOverview.tsx b/frontend/src/components/module-editor/SideBarModuleOverview.tsx
index ffee03f1..98ceee92 100644
--- a/frontend/src/components/module-editor/SideBarModuleOverview.tsx
+++ b/frontend/src/components/module-editor/SideBarModuleOverview.tsx
@@ -3,13 +3,41 @@ import { AddIcon } from "@chakra-ui/icons";
import React, { useContext, useState } from "react";
import { useParams } from "react-router-dom";
+import { DragDropContext, Droppable, DropResult } from "react-beautiful-dnd";
import EditorContext from "../../contexts/ModuleEditorContext";
-import { ModuleEditorParams } from "../../types/ModuleEditorTypes";
+import {
+ EditorContextAction,
+ ModuleEditorParams,
+} from "../../types/ModuleEditorTypes";
import { Modal } from "../common/Modal";
import { TextInput } from "../common/TextInput";
import LessonItem from "./LessonItem";
+// Copy drag implementation based on https://github.com/atlassian/react-beautiful-dnd/issues/216#issuecomment-423708497
+const onDragEnd = (
+ dispatch: React.Dispatch,
+ result: DropResult,
+ moduleID: number,
+) => {
+ const { source, destination } = result;
+ // dropped outside the list
+ if (
+ source?.droppableId !== "LESSON_EDITOR" ||
+ destination?.droppableId !== "LESSON_EDITOR"
+ ) {
+ return;
+ }
+ dispatch({
+ type: "reorder-lessons",
+ value: {
+ moduleID,
+ oldIndex: source.index,
+ newIndex: destination.index,
+ },
+ });
+};
+
const SideBarModuleOverview = (): React.ReactElement => {
const context = useContext(EditorContext);
const { isOpen, onOpen, onClose } = useDisclosure();
@@ -25,8 +53,6 @@ const SideBarModuleOverview = (): React.ReactElement => {
const { lessons, course, focusedLesson } = state;
const module = course.modules[moduleID];
- const orderedLessons = module.lessons.map((id) => lessons[id]);
-
const setFocus = (index: number) =>
dispatch({ type: "set-focus", value: module.lessons[index] });
@@ -57,15 +83,29 @@ const SideBarModuleOverview = (): React.ReactElement => {
return (
- {focusedLesson &&
- orderedLessons.map((lesson, index) => (
- setFocus(index)}
- />
- ))}
+ {focusedLesson && (
+ onDragEnd(dispatch, result, moduleID)}
+ >
+
+ {(provided) => (
+
+ {module.lessons.map((id, index) => (
+ setFocus(index)}
+ index={index}
+ />
+ ))}
+ {provided.placeholder}
+
+ )}
+
+
+ )}
{
+ // focusedLesson should exist in the same module as desired lesson
+ const lessonID = state.focusedLesson;
+ if (!lessonID) {
+ return state;
+ }
+
+ const oldModule = state.course.modules[moduleID];
+
+ console.assert(oldIndex >= 0, "Old lesson index must be positive");
+ console.assert(
+ oldIndex < oldModule.lessons.length,
+ "Old lesson index exceeds lesson length",
+ );
+ console.assert(newIndex >= 0, "New lesson index must be positive");
+ console.assert(
+ newIndex < oldModule.lessons.length,
+ "New lesson index exceeds lesson length",
+ );
+
+ const newLessons = [...state.course.modules[moduleID].lessons];
+ const [draggedLesson] = newLessons.splice(oldIndex, 1);
+ newLessons.splice(newIndex, 0, draggedLesson);
+ const newModules = [...state.course.modules];
+ newModules[moduleID] = { ...newModules[moduleID], lessons: newLessons };
+ const newState = {
+ ...state,
+ course: {
+ ...state.course,
+ modules: newModules,
+ },
+ };
+ newState.hasChanged = updateChangeStatus(
+ state.hasChanged,
+ "",
+ "COURSE-UPDATE",
+ );
+ return newState;
+};
+
const deleteLesson = (state: EditorStateType, id: string) => {
if (Object.keys(state.lessons).includes(id)) return state;
@@ -252,6 +297,13 @@ export default function EditorContextReducer(
return deleteLesson(state, action.value);
case "update-lesson-id":
return replaceLessonID(state, action.value.oldID, action.value.newID);
+ case "reorder-lessons":
+ return reorderLessons(
+ state,
+ action.value.moduleID,
+ action.value.oldIndex,
+ action.value.newIndex,
+ );
case "create-block":
return createLessonContentBlock(
state,
diff --git a/frontend/src/types/ModuleEditorTypes.ts b/frontend/src/types/ModuleEditorTypes.ts
index 248203e9..7ab24787 100644
--- a/frontend/src/types/ModuleEditorTypes.ts
+++ b/frontend/src/types/ModuleEditorTypes.ts
@@ -73,7 +73,11 @@ export interface ModuleEditorParams {
moduleIndex: string;
}
-export type EditorChangeStatus = "CREATE" | "UPDATE" | "DELETE";
+export type EditorChangeStatus =
+ | "CREATE"
+ | "UPDATE"
+ | "DELETE"
+ | "COURSE-UPDATE";
export interface EditorChangeStatuses {
[doc_id: string]: EditorChangeStatus;
@@ -113,6 +117,14 @@ export type EditorContextAction =
type: "delete-lesson";
value: string;
}
+ | {
+ type: "reorder-lessons";
+ value: {
+ moduleID: number;
+ oldIndex: number;
+ newIndex: number;
+ };
+ }
| {
type: "update-lesson-id";
value: { oldID: string; newID: string };