From 5a56d26b4d79d1424ea6c6f03c56c878a6e60576 Mon Sep 17 00:00:00 2001 From: Alex Mickelson Date: Mon, 7 Jul 2025 11:06:04 -0600 Subject: [PATCH] can manage course navigation from settinss --- docker-compose.yml | 30 ++++-- requests/nav.http | 4 + run.sh | 22 +++-- .../[courseName]/settings/AllSettings.tsx | 7 ++ .../CanvasNavigationManagement.tsx | 92 +++++++++++++++++++ .../canvasNavigation.tsx/NavTabListItem.tsx | 46 ++++++++++ src/app/providersQueryClientUtils.ts | 2 +- src/hooks/canvas/canvasModuleHooks.ts | 1 + src/hooks/canvas/canvasNavigationHooks.tsx | 42 +++++++++ src/services/canvas/canvasModuleService.ts | 1 + .../canvas/canvasNavigationService.ts | 34 +++++++ 11 files changed, 264 insertions(+), 17 deletions(-) create mode 100644 requests/nav.http create mode 100644 src/app/course/[courseName]/settings/canvasNavigation.tsx/CanvasNavigationManagement.tsx create mode 100644 src/app/course/[courseName]/settings/canvasNavigation.tsx/NavTabListItem.tsx create mode 100644 src/hooks/canvas/canvasNavigationHooks.tsx create mode 100644 src/services/canvas/canvasNavigationService.ts 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; + }, +};