can add new courses, kinda janky

This commit is contained in:
2025-07-22 15:09:10 -06:00
parent 704a5ae404
commit 46e0c36916
7 changed files with 197 additions and 109 deletions

View File

@@ -7,3 +7,5 @@ courses:
name: Web Intro
- path: ./1430/2025-fall-alex/modules/
name: UX
- path: ./1420/2025-fall-alex/labModules
name: "1425"

View File

@@ -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}
/>
)}
</SuspenseAndErrorHandling>
@@ -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<CanvasCourseModel | undefined>
setSelectedCanvasCourse: Dispatch<
SetStateAction<CanvasCourseModel | undefined>
>;
selectedDirectory: string | undefined;
setSelectedDirectory: React.Dispatch<
React.SetStateAction<string | undefined>
>;
setSelectedDirectory: Dispatch<SetStateAction<string | undefined>>;
selectedDaysOfWeek: DayOfWeek[];
setSelectedDaysOfWeek: React.Dispatch<React.SetStateAction<DayOfWeek[]>>;
setSelectedDaysOfWeek: Dispatch<SetStateAction<DayOfWeek[]>>;
courseToImport: LocalCourseSettings | undefined;
setCourseToImport: React.Dispatch<
React.SetStateAction<LocalCourseSettings | undefined>
>;
setCourseToImport: Dispatch<SetStateAction<LocalCourseSettings | undefined>>;
name: string;
setName: Dispatch<SetStateAction<string>>;
}) {
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}
/>
<SelectInput
value={selectedDirectory}
setValue={setSelectedDirectory}
<StoragePathSelector
value={directory}
setValue={setDirectory}
setLastTypedValue={setSelectedDirectory}
label={"Storage Folder"}
options={emptyDirectories ?? []}
getOptionName={(d) => d}
emptyOptionText="--- add a new folder to your docker compose to add more folders ---"
/>
<div className="px-5">
New folders will not be created automatically, you are expected to mount
a docker volume for each courses.
</div>
<br />
<div className="flex justify-center">
<DayOfWeekInput
@@ -228,6 +241,7 @@ function OtherSettings({
options={allSettings}
getOptionName={(c) => c.name}
/>
<TextInput value={name} setValue={setName} label={"Display Name"} />
<div className="px-5">
Assignments, Quizzes, Pages, and Lectures will have their due dates
moved based on how far they are from the start of the semester.

View File

@@ -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<HTMLInputElement>) => {
if (!isFocused || filteredFolders.length === 0) return;
if (e.key === "ArrowDown") {

View File

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

View File

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

View File

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

View File

@@ -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,16 +44,35 @@ 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
.mutation(
async ({
input: { settings, settingsFromCourseToImport, name, directory },
}) => {
console.log("creating in directory", directory);
await fileStorageService.settings.createCourseSettings(
settings,
directory
);
const globalSettings = await getGlobalSettings();
await updateGlobalSettings({
...globalSettings,
courses: [
...globalSettings.courses,
{
name,
path: directory,
},
],
});
if (settingsFromCourseToImport) {
const oldCourseName = settingsFromCourseToImport.name;
const newCourseName = settings.name;
@@ -82,14 +108,16 @@ export const settingsRouter = router({
const newAssignment = prepAssignmentForNewSemester(
oldAssignment,
settingsFromCourseToImport.startDate,
settings.startDate,
settings.startDate
);
await fileStorageService.assignments.updateOrCreateAssignment({
await fileStorageService.assignments.updateOrCreateAssignment(
{
courseName: newCourseName,
moduleName,
assignmentName: newAssignment.name,
assignment: newAssignment,
});
}
);
}),
...oldQuizzes.map(async (oldQuiz) => {
const newQuiz = prepQuizForNewSemester(
@@ -131,7 +159,8 @@ export const settingsRouter = router({
})
);
}
}),
}
),
updateSettings: publicProcedure
.input(
z.object({