diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml index e7329cc..2f5abe2 100644 --- a/docker-compose.dev.yml +++ b/docker-compose.dev.yml @@ -15,6 +15,7 @@ services: - NEXT_PUBLIC_ENABLE_FILE_SYNC=true - REDIS_URL=redis://redis:6379 volumes: + # - ./globalSettings.dev.yml:/app/globalSettings.yml - ./globalSettings.yml:/app/globalSettings.yml - .:/app - ~/projects/faculty:/app/storage diff --git a/docker-compose.yml b/docker-compose.yml index dc7d54f..b5cff1a 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -14,30 +14,8 @@ services: - REDIS_URL=redis://redis:6379 # - FILE_POLLING=true volumes: - # - ~/projects/faculty/3840_Telemetry/2024Spring_alex/modules:/app/storage/spring_2024_telemetry - - # - ~/projects/faculty/1400/2025_spring_alex/modules:/app/storage/1400 - # - ~/projects/faculty/1405/2025_spring_alex:/app/storage/1405 - # - ~/projects/faculty/3840_Telemetry/2025_spring_alex/modules:/app/storage/telemetry - # - ~/projects/faculty/4620_Distributed/2025Spring/modules:/app/storage/distributed - # - ~/projects/faculty/4620_Distributed/2024Spring/modules:/app/storage/distributed_old - - - - ~/projects/faculty/1810/2025-spring-alex/in-person:/app/storage/intro_to_web_old - - ~/projects/faculty/1810/2025-fall-alex/modules:/app/storage/intro_to_web - - - ~/projects/faculty/4850_AdvancedFE/2025-fall-alex/modules:/app/storage/advanced_frontend - - ~/projects/faculty/4850_AdvancedFE/2024-fall-alex/modules:/app/storage/advanced_frontend_old - - - ~/projects/faculty/1430/2024-fall-alex/modules:/app/storage/ux_old - - ~/projects/faculty/1430/2025-fall-alex/modules:/app/storage/ux - - - ~/projects/faculty/1420/2024-fall/Modules:/app/storage/1420_old - - ~/projects/faculty/1420/2025-fall-alex/modules:/app/storage/1420 - - - ~/projects/faculty/1425/2024-fall/Modules:/app/storage/1425_old - - ~/projects/faculty/1425/2025-fall-alex/modules:/app/storage/1425 - + - ./globalSettings.yml:/app/globalSettings.yml + - ~/projects/faculty:/app/storage - ~/projects/facultyFiles:/app/public/images/facultyFiles diff --git a/globalSettings.dev.yml b/globalSettings.dev.yml new file mode 100644 index 0000000..e144a29 --- /dev/null +++ b/globalSettings.dev.yml @@ -0,0 +1,5 @@ +courses: + - path: ./3820_BackEnd/2025-fall/Modules/ + name: Back-End + - path: ./3820_BackEnd/2024-fall/Modules/ + name: Back-End diff --git a/requests/module.http b/requests/module.http new file mode 100644 index 0000000..f172ffd --- /dev/null +++ b/requests/module.http @@ -0,0 +1,12 @@ + +### doesn't work, there is a form request that does thi son the site, but this isn't it +POST https://snow.instructure.com/api/v1/courses/1154759/modules/3965250/reorder +Authorization: Bearer {{$dotenv CANVAS_TOKEN}} +Content-Type: application/json + +{ + "order": [ + 29273428, 29274455, 29272498, 29274867, 29272743, 29273425 + ] +} + diff --git a/src/app/CourseList.tsx b/src/app/CourseList.tsx index e4c4963..2ded6fe 100644 --- a/src/app/CourseList.tsx +++ b/src/app/CourseList.tsx @@ -15,6 +15,8 @@ export default function CourseList() { const sortedDates = Object.keys(coursesByStartDate).sort(); + console.log(allSettings, coursesByStartDate); + return (
{sortedDates.map((startDate) => ( @@ -29,10 +31,10 @@ export default function CourseList() { href={getCourseUrl(settings.name)} shallow={true} className=" - font-bold text-xl block - transition-all hover:scale-105 hover:underline hover:text-slate-200 - mb-3 - " + font-bold text-xl block + transition-all hover:scale-105 hover:underline hover:text-slate-200 + mb-3 + " > {settings.name} diff --git a/src/app/course/[courseName]/modules/ExpandableModule.tsx b/src/app/course/[courseName]/modules/ExpandableModule.tsx index af90c56..2ad91b1 100644 --- a/src/app/course/[courseName]/modules/ExpandableModule.tsx +++ b/src/app/course/[courseName]/modules/ExpandableModule.tsx @@ -25,6 +25,9 @@ import { useQuizzesQueries } from "@/features/local/quizzes/quizHooks"; import { useTRPC } from "@/services/serverFunctions/trpcClient"; import { useSuspenseQueries } from "@tanstack/react-query"; import { useAssignmentNamesQuery } from "@/features/local/assignments/assignmentHooks"; +import { useReorderCanvasModuleItemsMutation } from "@/features/canvas/hooks/canvasModuleHooks"; +import { useCanvasModulesQuery } from "@/features/canvas/hooks/canvasModuleHooks"; +import { Spinner } from "@/components/Spinner"; export default function ExpandableModule({ moduleName, @@ -50,6 +53,8 @@ export default function ExpandableModule({ const { data: quizzes } = useQuizzesQueries(moduleName); const { data: pages } = usePagesQueries(moduleName); const modal = useModal(); + const reorderMutation = useReorderCanvasModuleItemsMutation(); + const { data: canvasModules } = useCanvasModulesQuery(); const moduleItems: { type: "assignment" | "quiz" | "page"; @@ -110,6 +115,30 @@ export default function ExpandableModule({ )} > <> + {!reorderMutation.isPending && ( + + )} + {reorderMutation.isPending && } ["canvas", canvasId, "module list"] as const, @@ -28,3 +30,54 @@ export const useAddCanvasModuleMutation = () => { }, }); }; + +export const useReorderCanvasModuleItemsMutation = () => { + const { data: settings } = useLocalCourseSettingsQuery(); + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: async ({ + moduleId, + items, + }: { + moduleId: number; + items: IModuleItem[]; + }) => { + if (!settings?.canvasId) throw new Error("No canvasId in settings"); + + const canvasModule = await canvasModuleService.getModuleWithItems( + settings.canvasId, + moduleId + ); + if (!canvasModule.items) { + throw new Error( + "cannot sort canvas module items, no items found in module" + ); + } + const canvasItems = canvasModule.items; + + // Sort IModuleItems by dueAt + const sorted = [...items].sort((a, b) => { + const aDate = a.dueAt ? new Date(a.dueAt).getTime() : 0; + const bDate = b.dueAt ? new Date(b.dueAt).getTime() : 0; + return aDate - bDate; + }); + + // Map sorted IModuleItems to CanvasModuleItem ids by matching name/title + const orderedIds = sorted + .map((localItem) => canvasItems.find((canvasItem) => canvasItem.title === localItem.name)?.id) + .filter((id): id is number => typeof id === "number"); + + return await canvasModuleService.reorderModuleItems( + settings.canvasId, + moduleId, + orderedIds + ); + }, + onSuccess: (_data) => { + if (!settings?.canvasId) return; + queryClient.invalidateQueries({ + queryKey: canvasCourseModuleKeys.modules(settings.canvasId), + }); + }, + }); +}; diff --git a/src/features/canvas/services/canvasModuleService.ts b/src/features/canvas/services/canvasModuleService.ts index e157324..62c9e69 100644 --- a/src/features/canvas/services/canvasModuleService.ts +++ b/src/features/canvas/services/canvasModuleService.ts @@ -20,6 +20,13 @@ export const canvasModuleService = { if (!data) throw new Error("Something went wrong updating module item"); }, + async getModuleWithItems(canvasCourseId: number, moduleId: number) { + const url = `${canvasApi}/courses/${canvasCourseId}/modules/${moduleId}`; + const params = { include: ["items"] }; + const response = await axiosClient.get(url, { params }); + return response.data; + }, + async createModuleItem( canvasCourseId: number, canvasModuleId: number, @@ -63,4 +70,21 @@ export const canvasModuleService = { const response = await axiosClient.post(url, body); return response.data.id; }, + + async reorderModuleItems( + canvasCourseId: number, + canvasModuleId: number, + itemIds: number[] + ) { + for (let i = 0; i < itemIds.length; i++) { + const itemId = itemIds[i]; + const url = `${canvasApi}/courses/${canvasCourseId}/modules/${canvasModuleId}/items/${itemId}`; + const body = { + module_item: { + position: i + 1, + }, + }; + await axiosClient.put(url, body); + } + }, }; diff --git a/src/features/local/course/settingsFileStorageService.ts b/src/features/local/course/settingsFileStorageService.ts index 3cba9e9..7520b45 100644 --- a/src/features/local/course/settingsFileStorageService.ts +++ b/src/features/local/course/settingsFileStorageService.ts @@ -15,7 +15,7 @@ import { GlobalSettingsCourse } from "../globalSettings/globalSettingsModels"; const getCourseSettings = async ( course: GlobalSettingsCourse ): Promise => { - const courseDirectory = await getCoursePathByName(course.name); + const courseDirectory = path.join(basePath, course.path); const settingsPath = path.join(courseDirectory, "settings.yml"); if (!(await directoryOrFileExists(settingsPath))) { const errorMessage = `could not find settings for ${course.name}, settings file ${settingsPath}`;