diff --git a/globalSettings.yml b/globalSettings.yml index 7832364..b2025bd 100644 --- a/globalSettings.yml +++ b/globalSettings.yml @@ -7,3 +7,5 @@ courses: name: Web Intro - path: ./1430/2025-fall-alex/modules/ name: UX + - path: ./1420/2025-fall-alex/labModules + name: "1425" diff --git a/src/app/addCourse/AddCourseToGlobalSettingsForm.tsx b/src/app/addCourse/AddCourseToGlobalSettingsForm.tsx index 154bde7..7756dbd 100644 --- a/src/app/addCourse/AddCourseToGlobalSettingsForm.tsx +++ b/src/app/addCourse/AddCourseToGlobalSettingsForm.tsx @@ -2,15 +2,24 @@ import ButtonSelect from "@/components/ButtonSelect"; import { DayOfWeekInput } from "@/components/form/DayOfWeekInput"; import SelectInput from "@/components/form/SelectInput"; +import { StoragePathSelector } from "@/components/form/StoragePathSelector"; +import TextInput from "@/components/form/TextInput"; import { Spinner } from "@/components/Spinner"; import { SuspenseAndErrorHandling } from "@/components/SuspenseAndErrorHandling"; import { useCourseListInTermQuery } from "@/hooks/canvas/canvasCourseHooks"; import { useCanvasTermsQuery } from "@/hooks/canvas/canvasHooks"; +import { + useGlobalSettingsQuery, + useUpdateGlobalSettingsMutation, +} from "@/hooks/localCourse/globalSettingsHooks"; import { useCreateLocalCourseMutation, useLocalCoursesSettingsQuery, } from "@/hooks/localCourse/localCoursesHooks"; -import { useEmptyDirectoriesQuery } from "@/hooks/localCourse/storageDirectoryHooks"; +import { + useDirectoryIsCourseQuery, + useEmptyDirectoriesQuery, +} from "@/hooks/localCourse/storageDirectoryHooks"; import { CanvasCourseModel } from "@/models/canvas/courses/canvasCourseModel"; import { CanvasEnrollmentTermModel } from "@/models/canvas/enrollmentTerms/canvasEnrollmentTermModel"; import { AssignmentSubmissionType } from "@/models/local/assignment/assignmentSubmissionType"; @@ -20,7 +29,7 @@ import { } from "@/models/local/localCourseSettings"; import { getCourseUrl } from "@/services/urlUtils"; import { useRouter } from "next/navigation"; -import React, { useMemo, useState } from "react"; +import React, { Dispatch, SetStateAction, useMemo, useState } from "react"; const sampleCompose = `services: canvas_manager: @@ -55,6 +64,7 @@ export default function AddNewCourseToGlobalSettingsForm() { const [courseToImport, setCourseToImport] = useState< LocalCourseSettings | undefined >(); + const [name, setName] = useState(""); const createCourse = useCreateLocalCourseMutation(); const formIsComplete = @@ -82,6 +92,8 @@ export default function AddNewCourseToGlobalSettingsForm() { setSelectedDaysOfWeek={setSelectedDaysOfWeek} courseToImport={courseToImport} setCourseToImport={setCourseToImport} + name={name} + setName={setName} /> )} @@ -90,6 +102,7 @@ export default function AddNewCourseToGlobalSettingsForm() { disabled={!formIsComplete || createCourse.isPending} onClick={async () => { if (formIsComplete) { + console.log("Creating course with settings:", selectedDirectory); const newSettings: LocalCourseSettings = courseToImport ? { ...courseToImport, @@ -128,8 +141,10 @@ export default function AddNewCourseToGlobalSettingsForm() { await createCourse.mutateAsync({ settings: newSettings, settingsFromCourseToImport: courseToImport, + name, + directory: selectedDirectory, }); - router.push(getCourseUrl(selectedDirectory)); + router.push(getCourseUrl(name)); } }} > @@ -156,26 +171,29 @@ function OtherSettings({ setSelectedDaysOfWeek, courseToImport, setCourseToImport, + name, + setName, }: { selectedTerm: CanvasEnrollmentTermModel; selectedCanvasCourse: CanvasCourseModel | undefined; - setSelectedCanvasCourse: React.Dispatch< - React.SetStateAction + setSelectedCanvasCourse: Dispatch< + SetStateAction >; selectedDirectory: string | undefined; - setSelectedDirectory: React.Dispatch< - React.SetStateAction - >; + setSelectedDirectory: Dispatch>; selectedDaysOfWeek: DayOfWeek[]; - setSelectedDaysOfWeek: React.Dispatch>; + setSelectedDaysOfWeek: Dispatch>; courseToImport: LocalCourseSettings | undefined; - setCourseToImport: React.Dispatch< - React.SetStateAction - >; + setCourseToImport: Dispatch>; + name: string; + setName: Dispatch>; }) { const { data: canvasCourses } = useCourseListInTermQuery(selectedTerm.id); const { data: allSettings } = useLocalCoursesSettingsQuery(); - const { data: emptyDirectories } = useEmptyDirectoriesQuery(); + const [directory, setDirectory] = useState("./"); + // const directoryIsCourseQuery = useDirectoryIsCourseQuery( + // selectedDirectory ?? "./" + // ); const populatedCanvasCourseIds = allSettings?.map((s) => s.canvasId) ?? []; const availableCourses = @@ -194,18 +212,13 @@ function OtherSettings({ getOptionName={(c) => c?.name ?? ""} center={true} /> - d} - emptyOptionText="--- add a new folder to your docker compose to add more folders ---" /> -
- New folders will not be created automatically, you are expected to mount - a docker volume for each courses. -

c.name} /> +
Assignments, Quizzes, Pages, and Lectures will have their due dates moved based on how far they are from the start of the semester. diff --git a/src/components/form/StoragePathSelector.tsx b/src/components/form/StoragePathSelector.tsx index 2d0f504..5c72ebf 100644 --- a/src/components/form/StoragePathSelector.tsx +++ b/src/components/form/StoragePathSelector.tsx @@ -7,11 +7,13 @@ export function StoragePathSelector({ setValue, label, className, + setLastTypedValue, }: { value: string; setValue: (newValue: string) => void; label: string; className?: string; + setLastTypedValue?: (value: string) => void; }) { const [path, setPath] = useState(value); const { data: directoryContents } = useDirectoryContentsQuery(value); @@ -25,6 +27,10 @@ export function StoragePathSelector({ setPath(value); }, [value]); + useEffect(() => { + if (setLastTypedValue) setLastTypedValue(path); + }, [path, setLastTypedValue]); + const handleKeyDown = (e: React.KeyboardEvent) => { if (!isFocused || filteredFolders.length === 0) return; if (e.key === "ArrowDown") { diff --git a/src/hooks/localCourse/localCoursesHooks.ts b/src/hooks/localCourse/localCoursesHooks.ts index 6fb3a02..1dfc039 100644 --- a/src/hooks/localCourse/localCoursesHooks.ts +++ b/src/hooks/localCourse/localCoursesHooks.ts @@ -6,6 +6,7 @@ import { useMutation, useQueryClient, } from "@tanstack/react-query"; +import { useGlobalSettingsQuery } from "./globalSettingsHooks"; export const useLocalCoursesSettingsQuery = () => { const trpc = useTRPC(); @@ -23,6 +24,7 @@ export const useLocalCourseSettingsQuery = () => { export const useCreateLocalCourseMutation = () => { const trpc = useTRPC(); const queryClient = useQueryClient(); + return useMutation( trpc.settings.createCourse.mutationOptions({ onSuccess: () => { @@ -32,6 +34,9 @@ export const useCreateLocalCourseMutation = () => { queryClient.invalidateQueries({ queryKey: trpc.directories.getEmptyDirectories.queryKey(), }); + queryClient.invalidateQueries({ + queryKey: trpc.globalSettings.getGlobalSettings.queryKey(), + }); }, }) ); diff --git a/src/services/fileStorage/globalSettingsFileStorageService.ts b/src/services/fileStorage/globalSettingsFileStorageService.ts index 2caf66a..05d5237 100644 --- a/src/services/fileStorage/globalSettingsFileStorageService.ts +++ b/src/services/fileStorage/globalSettingsFileStorageService.ts @@ -1,4 +1,7 @@ -import { GlobalSettings } from "@/models/local/globalSettings"; +import { + GlobalSettings, + zodGlobalSettings, +} from "@/models/local/globalSettings"; import { globalSettingsToYaml, parseGlobalSettingsYaml, @@ -39,6 +42,15 @@ export const getCoursePathByName = async (courseName: string) => { }; export const updateGlobalSettings = async (globalSettings: GlobalSettings) => { - const globalSettingsString = globalSettingsToYaml(globalSettings); + const globalSettingsString = globalSettingsToYaml( + zodGlobalSettings.parse(globalSettings) + ); await fs.writeFile(SETTINGS_FILE_PATH, globalSettingsString, "utf-8"); + + // await Promise.all( + // globalSettings.courses.map(async (course) => { + // const coursePath = await getCoursePathByName(course.name); + // await fs.mkdir(coursePath, { recursive: true }); + // }) + // ); }; diff --git a/src/services/fileStorage/settingsFileStorageService.ts b/src/services/fileStorage/settingsFileStorageService.ts index 2613684..7e81cdf 100644 --- a/src/services/fileStorage/settingsFileStorageService.ts +++ b/src/services/fileStorage/settingsFileStorageService.ts @@ -86,6 +86,26 @@ export const settingsFileStorageService = { console.log(`Saving settings ${settingsPath}`); await fs.writeFile(settingsPath, settingsMarkdown); }, + async createCourseSettings(settings: LocalCourseSettings, directory: string) { + const courseDirectory = path.join(basePath, directory); + + if (await directoryOrFileExists(courseDirectory)) { + throw new Error( + `Course path "${courseDirectory}" already exists. Create course in a new folder.` + ); + } + + await fs.mkdir(courseDirectory, { recursive: true }); + const settingsPath = path.join(courseDirectory, "settings.yml"); + + const { name: _, ...settingsWithoutName } = settings; + + const settingsMarkdown = + localCourseYamlUtils.settingsToYaml(settingsWithoutName); + + console.log(`Saving settings ${settingsPath}`); + await fs.writeFile(settingsPath, settingsMarkdown); + }, async folderIsCourse(folderPath: string) { const settingsPath = path.join(basePath, folderPath, "settings.yml"); if (!(await directoryOrFileExists(settingsPath))) { diff --git a/src/services/serverFunctions/router/settingsRouter.ts b/src/services/serverFunctions/router/settingsRouter.ts index ae0849b..39f4096 100644 --- a/src/services/serverFunctions/router/settingsRouter.ts +++ b/src/services/serverFunctions/router/settingsRouter.ts @@ -13,6 +13,13 @@ import { prepPageForNewSemester, prepQuizForNewSemester, } from "@/models/local/utils/semesterTransferUtils"; +import { + getGlobalSettings, + updateGlobalSettings, +} from "@/services/fileStorage/globalSettingsFileStorageService"; +import { promises as fs } from "fs"; +import { basePath } from "@/services/fileStorage/utils/fileSystemUtils"; +import path from "path"; export const settingsRouter = router({ allCoursesSettings: publicProcedure.query(async () => { @@ -37,101 +44,123 @@ export const settingsRouter = router({ createCourse: publicProcedure .input( z.object({ + name: z.string(), + directory: z.string(), settings: zodLocalCourseSettings, settingsFromCourseToImport: zodLocalCourseSettings.optional(), }) ) - .mutation(async ({ input: { settings, settingsFromCourseToImport } }) => { - await fileStorageService.settings.updateCourseSettings( - settings.name, - settings - ); - - if (settingsFromCourseToImport) { - const oldCourseName = settingsFromCourseToImport.name; - const newCourseName = settings.name; - const oldModules = await fileStorageService.modules.getModuleNames( - oldCourseName + .mutation( + async ({ + input: { settings, settingsFromCourseToImport, name, directory }, + }) => { + console.log("creating in directory", directory); + await fileStorageService.settings.createCourseSettings( + settings, + directory ); - await Promise.all( - oldModules.map(async (moduleName) => { - await fileStorageService.modules.createModule( - newCourseName, - moduleName - ); - const [oldAssignments, oldQuizzes, oldPages, oldLecturesByWeek] = + const globalSettings = await getGlobalSettings(); + + await updateGlobalSettings({ + ...globalSettings, + courses: [ + ...globalSettings.courses, + { + name, + path: directory, + }, + ], + }); + + if (settingsFromCourseToImport) { + const oldCourseName = settingsFromCourseToImport.name; + const newCourseName = settings.name; + const oldModules = await fileStorageService.modules.getModuleNames( + oldCourseName + ); + await Promise.all( + oldModules.map(async (moduleName) => { + await fileStorageService.modules.createModule( + newCourseName, + moduleName + ); + + const [oldAssignments, oldQuizzes, oldPages, oldLecturesByWeek] = + await Promise.all([ + fileStorageService.assignments.getAssignments( + oldCourseName, + moduleName + ), + await fileStorageService.quizzes.getQuizzes( + oldCourseName, + moduleName + ), + await fileStorageService.pages.getPages( + oldCourseName, + moduleName + ), + await getLectures(oldCourseName), + ]); + await Promise.all([ - fileStorageService.assignments.getAssignments( - oldCourseName, - moduleName - ), - await fileStorageService.quizzes.getQuizzes( - oldCourseName, - moduleName - ), - await fileStorageService.pages.getPages( - oldCourseName, - moduleName - ), - await getLectures(oldCourseName), - ]); - - await Promise.all([ - ...oldAssignments.map(async (oldAssignment) => { - const newAssignment = prepAssignmentForNewSemester( - oldAssignment, - settingsFromCourseToImport.startDate, - settings.startDate, - ); - await fileStorageService.assignments.updateOrCreateAssignment({ - courseName: newCourseName, - moduleName, - assignmentName: newAssignment.name, - assignment: newAssignment, - }); - }), - ...oldQuizzes.map(async (oldQuiz) => { - const newQuiz = prepQuizForNewSemester( - oldQuiz, - settingsFromCourseToImport.startDate, - settings.startDate - ); - await fileStorageService.quizzes.updateQuiz({ - courseName: newCourseName, - moduleName, - quizName: newQuiz.name, - quiz: newQuiz, - }); - }), - ...oldPages.map(async (oldPage) => { - const newPage = prepPageForNewSemester( - oldPage, - settingsFromCourseToImport.startDate, - settings.startDate - ); - await fileStorageService.pages.updatePage({ - courseName: newCourseName, - moduleName, - pageName: newPage.name, - page: newPage, - }); - }), - ...oldLecturesByWeek.flatMap(async (oldLectureByWeek) => - oldLectureByWeek.lectures.map(async (oldLecture) => { - const newLecture = prepLectureForNewSemester( - oldLecture, + ...oldAssignments.map(async (oldAssignment) => { + const newAssignment = prepAssignmentForNewSemester( + oldAssignment, settingsFromCourseToImport.startDate, settings.startDate ); - await updateLecture(newCourseName, settings, newLecture); - }) - ), - ]); - }) - ); + await fileStorageService.assignments.updateOrCreateAssignment( + { + courseName: newCourseName, + moduleName, + assignmentName: newAssignment.name, + assignment: newAssignment, + } + ); + }), + ...oldQuizzes.map(async (oldQuiz) => { + const newQuiz = prepQuizForNewSemester( + oldQuiz, + settingsFromCourseToImport.startDate, + settings.startDate + ); + await fileStorageService.quizzes.updateQuiz({ + courseName: newCourseName, + moduleName, + quizName: newQuiz.name, + quiz: newQuiz, + }); + }), + ...oldPages.map(async (oldPage) => { + const newPage = prepPageForNewSemester( + oldPage, + settingsFromCourseToImport.startDate, + settings.startDate + ); + await fileStorageService.pages.updatePage({ + courseName: newCourseName, + moduleName, + pageName: newPage.name, + page: newPage, + }); + }), + ...oldLecturesByWeek.flatMap(async (oldLectureByWeek) => + oldLectureByWeek.lectures.map(async (oldLecture) => { + const newLecture = prepLectureForNewSemester( + oldLecture, + settingsFromCourseToImport.startDate, + settings.startDate + ); + await updateLecture(newCourseName, settings, newLecture); + }) + ), + ]); + }) + ); + } } - }), + ), updateSettings: publicProcedure .input( z.object({