diff --git a/docker-compose.yml b/docker-compose.yml
index d8fafe7..8d48243 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -13,15 +13,29 @@ services:
- NEXT_PUBLIC_ENABLE_FILE_SYNC=true
# - FILE_POLLING=true
volumes:
- - ~/projects/faculty/3840_Telemetry/2024Spring_alex/modules:/app/storage/spring_2024_telemetry
+ # - ~/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
- - ~/projects/faculty/1810/2025-spring-alex/online:/app/storage/intro_to_web_online
+ # - ~/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
- ~/projects/facultyFiles:/app/public/images/facultyFiles
diff --git a/requests/nav.http b/requests/nav.http
new file mode 100644
index 0000000..83380e1
--- /dev/null
+++ b/requests/nav.http
@@ -0,0 +1,4 @@
+# https://developerdocs.instructure.com/services/canvas/file.all_resources/tabs#method.tabs.index
+###
+GET https://snow.instructure.com/api/v1/courses/1155200/tabs
+Authorization: Bearer {{$dotenv CANVAS_TOKEN}}
\ No newline at end of file
diff --git a/run.sh b/run.sh
index 0356113..387412a 100755
--- a/run.sh
+++ b/run.sh
@@ -9,14 +9,11 @@ docker run -it --rm \
-p 3000:3000 \
-w /app \
-v .:/app \
- -v ~/projects/faculty/4850_AdvancedFE/2024-fall-alex/modules:/app/storage/advanced_frontend \
- -v ~/projects/faculty/1810/2025-spring-alex/online:/app/storage/intro_to_web_online \
- -v ~/projects/faculty/1810/2025-spring-alex/in-person:/app/storage/intro_to_web \
- -v ~/projects/faculty/1400/2025_spring_alex/modules:/app/storage/1400 \
- -v ~/projects/faculty/1405/2025_spring_alex:/app/storage/1405 \
- -v ~/projects/faculty/3840_Telemetry/2025_spring_alex/modules:/app/storage/telemetry \
- -v ~/projects/faculty/4620_Distributed/2025Spring/modules:/app/storage/distributed \
- -v ~/projects/faculty/1430/2025-spring-jonathan/Modules:/app/storage/jonathan-ux \
+ -v ~/projects/faculty/4850_AdvancedFE/2025-fall-alex/modules:/app/storage/advanced_frontend \
+ -v ~/projects/faculty/1810/2025-fall-alex/modules:/app/storage/intro_to_web \
+ -v ~/projects/faculty/1430/2025-fall-alex/modules:/app/storage/ux \
+ -v ~/projects/faculty/1420/2025-fall-alex/modules:/app/storage/1420 \
+ -v ~/projects/faculty/1425/2025-fall-alex/modules:/app/storage/1425 \
-v ~/projects/public:/app/public/images/public \
-v ~/projects/facultyFiles:/app/public/images/facultyFiles \
node \
@@ -29,5 +26,14 @@ docker run -it --rm \
pnpm install && pnpm dev
"
+ # -v ~/projects/faculty/4850_AdvancedFE/2024-fall-alex/modules:/app/storage/advanced_frontend_old \
+ # -v ~/projects/faculty/1810/2025-spring-alex/in-person:/app/storage/intro_to_web_old \
+ # -v ~/projects/faculty/1400/2025_spring_alex/modules:/app/storage/1400 \
+ # -v ~/projects/faculty/1405/2025_spring_alex:/app/storage/1405 \
+ # -v ~/projects/faculty/3840_Telemetry/2025_spring_alex/modules:/app/storage/telemetry \
+ # -v ~/projects/faculty/4620_Distributed/2025Spring/modules:/app/storage/distributed \
+ # -v ~/projects/faculty/1430/2024-fall-alex/modules:/app/storage/ux_old \
+ # -v ~/projects/faculty/1420/2024-fall/Modules:/app/storage/1420_old \
+ # -v ~/projects/faculty/1425/2024-fall/Modules:/app/storage/1425_old \
# bash -c "npm i -g pnpm && pnpm i && pnpm run dev -- -H 0.0.0.0"
diff --git a/src/app/course/[courseName]/settings/AllSettings.tsx b/src/app/course/[courseName]/settings/AllSettings.tsx
index f063c3f..5d6bab8 100644
--- a/src/app/course/[courseName]/settings/AllSettings.tsx
+++ b/src/app/course/[courseName]/settings/AllSettings.tsx
@@ -1,6 +1,7 @@
"use client";
import AssignmentGroupManagement from "./AssignmentGroupManagement";
+import { CanvasNavigationManagement } from "./canvasNavigation.tsx/CanvasNavigationManagement";
import DaysOfWeekSettings from "./DaysOfWeekSettings";
import DefaultDueTime from "./DefaultDueTime";
import DefaultFileUploadTypes from "./DefaultFileUploadTypes";
@@ -22,6 +23,12 @@ export default function AllSettings() {
+
+
+
+
+
+
>
);
}
diff --git a/src/app/course/[courseName]/settings/canvasNavigation.tsx/CanvasNavigationManagement.tsx b/src/app/course/[courseName]/settings/canvasNavigation.tsx/CanvasNavigationManagement.tsx
new file mode 100644
index 0000000..ee0d4e3
--- /dev/null
+++ b/src/app/course/[courseName]/settings/canvasNavigation.tsx/CanvasNavigationManagement.tsx
@@ -0,0 +1,92 @@
+import React, { useState } from "react";
+import { useCanvasTabsQuery } from "@/hooks/canvas/canvasNavigationHooks";
+import { CanvasCourseTab } from "@/services/canvas/canvasNavigationService";
+import { useUpdateCanvasTabMutation } from "@/hooks/canvas/canvasNavigationHooks";
+import { Spinner } from "@/components/Spinner";
+import { NavTabListItem } from "./NavTabListItem";
+
+export const CanvasNavigationManagement = () => {
+ const { data: tabs, isLoading, isError } = useCanvasTabsQuery();
+ const [draggedIndex, setDraggedIndex] = useState(null);
+ const updateTab = useUpdateCanvasTabMutation();
+
+ const handleHideAllExternal = async () => {
+ if (!tabs) return;
+ for (const tab of tabs.filter(
+ (tab) => tab.type.toLowerCase() === "external" && !tab.hidden
+ )) {
+ await updateTab.mutateAsync({
+ tabId: tab.id,
+ hidden: true,
+ position: tab.position,
+ });
+ }
+ };
+
+ if (isLoading) return Loading tabs...
;
+ if (isError || !tabs) return Error loading tabs.
;
+
+ const handleDragStart = (idx: number) => setDraggedIndex(idx);
+ const handleDrop = async (dropIdx: number) => {
+ if (draggedIndex === null || draggedIndex === dropIdx || !tabs) {
+ setDraggedIndex(null);
+ return;
+ }
+ const newTabs = [...tabs].sort((a, b) => a.position - b.position);
+ const [removed] = newTabs.splice(draggedIndex, 1);
+ newTabs.splice(dropIdx, 0, removed);
+ // Persist new order
+ for (let i = 0; i < newTabs.length; i++) {
+ const tab = newTabs[i];
+ if (tab.position !== i + 1) {
+ await updateTab.mutateAsync({
+ tabId: tab.id,
+ position: i + 1,
+ hidden: tab.hidden,
+ });
+ }
+ }
+ setDraggedIndex(null);
+ };
+
+ return (
+
+
+
+ Manage Course Navigation Tabs
+
+
+ {updateTab.isPending ? (
+
+ ) : (
+
+ )}
+
+
+
+
+ {[...tabs]
+ .sort((a, b) => a.position - b.position)
+ .map((tab, idx) => (
+ handleDragStart(idx)}
+ onDragOver={(e) => {
+ e.preventDefault();
+ }}
+ onDrop={() => handleDrop(idx)}
+ />
+ ))}
+
+
+
+ );
+};
diff --git a/src/app/course/[courseName]/settings/canvasNavigation.tsx/NavTabListItem.tsx b/src/app/course/[courseName]/settings/canvasNavigation.tsx/NavTabListItem.tsx
new file mode 100644
index 0000000..b4c7ff9
--- /dev/null
+++ b/src/app/course/[courseName]/settings/canvasNavigation.tsx/NavTabListItem.tsx
@@ -0,0 +1,46 @@
+import { Spinner } from "@/components/Spinner";
+import { useUpdateCanvasTabMutation } from "@/hooks/canvas/canvasNavigationHooks";
+import { CanvasCourseTab } from "@/services/canvas/canvasNavigationService";
+import React, { FC } from "react";
+
+export const NavTabListItem: FC<{
+ tab: CanvasCourseTab;
+ idx: number;
+ onDragStart: () => void;
+ onDragOver: (e: React.DragEvent) => void;
+ onDrop: () => void;
+}> = ({ tab, idx, onDragStart, onDragOver, onDrop }) => {
+ const updateTab = useUpdateCanvasTabMutation();
+ const handleToggleVisibility = () => {
+ updateTab.mutate({
+ tabId: tab.id,
+ hidden: !tab.hidden,
+ position: tab.position,
+ });
+ };
+ return (
+
+ {tab.label}
+ {updateTab.isPending && }
+ {tab.type}
+
+
+ );
+};
diff --git a/src/app/providersQueryClientUtils.ts b/src/app/providersQueryClientUtils.ts
index 8bca402..fc2ffea 100644
--- a/src/app/providersQueryClientUtils.ts
+++ b/src/app/providersQueryClientUtils.ts
@@ -14,7 +14,7 @@ export function makeQueryClient() {
// refetchInterval: 7000, // debug only
refetchOnWindowFocus: false,
retry: 0,
- refetchOnMount: false,
+ // refetchOnMount: false,
},
mutations: {
onError: (error) => {
diff --git a/src/hooks/canvas/canvasModuleHooks.ts b/src/hooks/canvas/canvasModuleHooks.ts
index 0903f3f..0a6a8ae 100644
--- a/src/hooks/canvas/canvasModuleHooks.ts
+++ b/src/hooks/canvas/canvasModuleHooks.ts
@@ -32,3 +32,4 @@ export const useAddCanvasModuleMutation = () => {
},
});
};
+
diff --git a/src/hooks/canvas/canvasNavigationHooks.tsx b/src/hooks/canvas/canvasNavigationHooks.tsx
new file mode 100644
index 0000000..a00c639
--- /dev/null
+++ b/src/hooks/canvas/canvasNavigationHooks.tsx
@@ -0,0 +1,42 @@
+import { useQuery, useQueryClient, useMutation } from "@tanstack/react-query";
+import { useLocalCourseSettingsQuery } from "../localCourse/localCoursesHooks";
+import { canvasNavigationService } from "@/services/canvas/canvasNavigationService";
+
+export const canvasCourseTabKeys = {
+ tabs: (canvasId: number) => ["canvas", canvasId, "tabs list"] as const,
+};
+
+export const useCanvasTabsQuery = () => {
+ const [settings] = useLocalCourseSettingsQuery();
+ return useQuery({
+ queryKey: canvasCourseTabKeys.tabs(settings.canvasId),
+ queryFn: async () =>
+ await canvasNavigationService.getCourseTabs(settings.canvasId),
+ });
+};
+
+export const useUpdateCanvasTabMutation = () => {
+ const [settings] = useLocalCourseSettingsQuery();
+ const queryClient = useQueryClient();
+ return useMutation({
+ mutationFn: async ({
+ tabId,
+ hidden,
+ position,
+ }: {
+ tabId: string;
+ hidden?: boolean;
+ position?: number;
+ }) =>
+ await canvasNavigationService.updateCourseTab(settings.canvasId, tabId, {
+ hidden,
+ position,
+ }),
+ onSuccess: () => {
+ queryClient.invalidateQueries({
+ queryKey: canvasCourseTabKeys.tabs(settings.canvasId),
+ refetchType: "all"
+ });
+ },
+ });
+};
diff --git a/src/services/canvas/canvasModuleService.ts b/src/services/canvas/canvasModuleService.ts
index 06b3153..c8307f1 100644
--- a/src/services/canvas/canvasModuleService.ts
+++ b/src/services/canvas/canvasModuleService.ts
@@ -63,4 +63,5 @@ export const canvasModuleService = {
const response = await axiosClient.post(url, body);
return response.data.id;
},
+
};
diff --git a/src/services/canvas/canvasNavigationService.ts b/src/services/canvas/canvasNavigationService.ts
new file mode 100644
index 0000000..7a7e5fb
--- /dev/null
+++ b/src/services/canvas/canvasNavigationService.ts
@@ -0,0 +1,34 @@
+import { axiosClient } from "../axiosUtils";
+import { canvasApi } from "./canvasServiceUtils";
+
+export interface CanvasCourseTab {
+ id: string;
+ html_url: string;
+ full_url: string;
+ position: number;
+ visibility: "public" | "members" | "admins" | "none";
+ label: string;
+ type: "internal" | "external";
+ hidden?: boolean;
+ unused?: boolean;
+ url?: string;
+}
+
+export const canvasNavigationService = {
+ async getCourseTabs(canvasCourseId: number) {
+ const url = `${canvasApi}/courses/${canvasCourseId}/tabs`;
+ const { data } = await axiosClient.get(url);
+ return data;
+ },
+
+ async updateCourseTab(
+ canvasCourseId: number,
+ tabId: string,
+ params: { hidden?: boolean; position?: number }
+ ) {
+ const url = `${canvasApi}/courses/${canvasCourseId}/tabs/${tabId}`;
+ const body = { ...params };
+ const { data } = await axiosClient.put(url, body);
+ return data;
+ },
+};