mirror of
https://github.com/alexmickelson/canvasManagement.git
synced 2026-03-25 23:28:33 -06:00
can add new courses, kinda janky
This commit is contained in:
@@ -7,3 +7,5 @@ courses:
|
||||
name: Web Intro
|
||||
- path: ./1430/2025-fall-alex/modules/
|
||||
name: UX
|
||||
- path: ./1420/2025-fall-alex/labModules
|
||||
name: "1425"
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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") {
|
||||
|
||||
@@ -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(),
|
||||
});
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
@@ -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 });
|
||||
// })
|
||||
// );
|
||||
};
|
||||
|
||||
@@ -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))) {
|
||||
|
||||
@@ -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({
|
||||
|
||||
Reference in New Issue
Block a user