mirror of
https://github.com/alexmickelson/canvasManagement.git
synced 2026-03-25 23:28:33 -06:00
can manage course navigation from settinss
This commit is contained in:
@@ -13,15 +13,29 @@ services:
|
|||||||
- NEXT_PUBLIC_ENABLE_FILE_SYNC=true
|
- NEXT_PUBLIC_ENABLE_FILE_SYNC=true
|
||||||
# - FILE_POLLING=true
|
# - FILE_POLLING=true
|
||||||
volumes:
|
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/1400/2025_spring_alex/modules:/app/storage/1400
|
||||||
- ~/projects/faculty/1405/2025_spring_alex:/app/storage/1405
|
# - ~/projects/faculty/1405/2025_spring_alex:/app/storage/1405
|
||||||
- ~/projects/faculty/3840_Telemetry/2025_spring_alex/modules:/app/storage/telemetry
|
# - ~/projects/faculty/3840_Telemetry/2025_spring_alex/modules:/app/storage/telemetry
|
||||||
- ~/projects/faculty/4620_Distributed/2025Spring/modules:/app/storage/distributed
|
# - ~/projects/faculty/4620_Distributed/2025Spring/modules:/app/storage/distributed
|
||||||
- ~/projects/faculty/4620_Distributed/2024Spring/modules:/app/storage/distributed_old
|
# - ~/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/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
|
- ~/projects/facultyFiles:/app/public/images/facultyFiles
|
||||||
|
|
||||||
|
|||||||
4
requests/nav.http
Normal file
4
requests/nav.http
Normal file
@@ -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}}
|
||||||
22
run.sh
22
run.sh
@@ -9,14 +9,11 @@ docker run -it --rm \
|
|||||||
-p 3000:3000 \
|
-p 3000:3000 \
|
||||||
-w /app \
|
-w /app \
|
||||||
-v .:/app \
|
-v .:/app \
|
||||||
-v ~/projects/faculty/4850_AdvancedFE/2024-fall-alex/modules:/app/storage/advanced_frontend \
|
-v ~/projects/faculty/4850_AdvancedFE/2025-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-fall-alex/modules:/app/storage/intro_to_web \
|
||||||
-v ~/projects/faculty/1810/2025-spring-alex/in-person:/app/storage/intro_to_web \
|
-v ~/projects/faculty/1430/2025-fall-alex/modules:/app/storage/ux \
|
||||||
-v ~/projects/faculty/1400/2025_spring_alex/modules:/app/storage/1400 \
|
-v ~/projects/faculty/1420/2025-fall-alex/modules:/app/storage/1420 \
|
||||||
-v ~/projects/faculty/1405/2025_spring_alex:/app/storage/1405 \
|
-v ~/projects/faculty/1425/2025-fall-alex/modules:/app/storage/1425 \
|
||||||
-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/public:/app/public/images/public \
|
-v ~/projects/public:/app/public/images/public \
|
||||||
-v ~/projects/facultyFiles:/app/public/images/facultyFiles \
|
-v ~/projects/facultyFiles:/app/public/images/facultyFiles \
|
||||||
node \
|
node \
|
||||||
@@ -29,5 +26,14 @@ docker run -it --rm \
|
|||||||
pnpm install && pnpm dev
|
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"
|
# bash -c "npm i -g pnpm && pnpm i && pnpm run dev -- -H 0.0.0.0"
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import AssignmentGroupManagement from "./AssignmentGroupManagement";
|
import AssignmentGroupManagement from "./AssignmentGroupManagement";
|
||||||
|
import { CanvasNavigationManagement } from "./canvasNavigation.tsx/CanvasNavigationManagement";
|
||||||
import DaysOfWeekSettings from "./DaysOfWeekSettings";
|
import DaysOfWeekSettings from "./DaysOfWeekSettings";
|
||||||
import DefaultDueTime from "./DefaultDueTime";
|
import DefaultDueTime from "./DefaultDueTime";
|
||||||
import DefaultFileUploadTypes from "./DefaultFileUploadTypes";
|
import DefaultFileUploadTypes from "./DefaultFileUploadTypes";
|
||||||
@@ -22,6 +23,12 @@ export default function AllSettings() {
|
|||||||
<DefaultDueTime />
|
<DefaultDueTime />
|
||||||
<AssignmentGroupManagement />
|
<AssignmentGroupManagement />
|
||||||
<HolidayConfig />
|
<HolidayConfig />
|
||||||
|
<CanvasNavigationManagement />
|
||||||
|
<div className="p-16"></div>
|
||||||
|
<div className="p-16"></div>
|
||||||
|
<div className="p-16"></div>
|
||||||
|
<div className="p-16"></div>
|
||||||
|
<div className="p-16"></div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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<number | null>(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 <div>Loading tabs...</div>;
|
||||||
|
if (isError || !tabs) return <div>Error loading tabs.</div>;
|
||||||
|
|
||||||
|
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 (
|
||||||
|
<div className=" mx-auto p-4">
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<h2 className="text-xl font-bold mb-4">
|
||||||
|
Manage Course Navigation Tabs
|
||||||
|
</h2>
|
||||||
|
<div>
|
||||||
|
{updateTab.isPending ? (
|
||||||
|
<Spinner />
|
||||||
|
) : (
|
||||||
|
<button
|
||||||
|
className="mb-4 px-3 py-1 rounded bg-slate-700 hover:bg-slate-600 text-white"
|
||||||
|
onClick={handleHideAllExternal}
|
||||||
|
disabled={updateTab.isPending}
|
||||||
|
>
|
||||||
|
Hide All External
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-center ">
|
||||||
|
<ul className="w-md h-[800px] overflow-y-auto rounded bg-slate-950 p-4 border border-slate-700">
|
||||||
|
{[...tabs]
|
||||||
|
.sort((a, b) => a.position - b.position)
|
||||||
|
.map((tab, idx) => (
|
||||||
|
<NavTabListItem
|
||||||
|
key={tab.id}
|
||||||
|
tab={tab}
|
||||||
|
idx={idx}
|
||||||
|
onDragStart={() => handleDragStart(idx)}
|
||||||
|
onDragOver={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
}}
|
||||||
|
onDrop={() => handleDrop(idx)}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -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 (
|
||||||
|
<li
|
||||||
|
key={tab.id}
|
||||||
|
className={`flex items-center justify-between mb-2 p-1 px-4 rounded bg-slate-800 ${
|
||||||
|
tab.hidden ? "opacity-50" : ""
|
||||||
|
}`}
|
||||||
|
draggable
|
||||||
|
onDragStart={onDragStart}
|
||||||
|
onDragOver={onDragOver}
|
||||||
|
onDrop={onDrop}
|
||||||
|
>
|
||||||
|
<span className="flex-1 cursor-move">{tab.label}</span>
|
||||||
|
{updateTab.isPending && <Spinner />}
|
||||||
|
<span className="text-xs text-slate-400 mr-2">{tab.type}</span>
|
||||||
|
<button
|
||||||
|
className={` py-1 rounded unstyled w-20 ${
|
||||||
|
tab.hidden ? "bg-slate-600" : "bg-blue-900/50"
|
||||||
|
}`}
|
||||||
|
onClick={handleToggleVisibility}
|
||||||
|
disabled={updateTab.isPending}
|
||||||
|
>
|
||||||
|
{tab.hidden ? "Show" : "Hide"}
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -14,7 +14,7 @@ export function makeQueryClient() {
|
|||||||
// refetchInterval: 7000, // debug only
|
// refetchInterval: 7000, // debug only
|
||||||
refetchOnWindowFocus: false,
|
refetchOnWindowFocus: false,
|
||||||
retry: 0,
|
retry: 0,
|
||||||
refetchOnMount: false,
|
// refetchOnMount: false,
|
||||||
},
|
},
|
||||||
mutations: {
|
mutations: {
|
||||||
onError: (error) => {
|
onError: (error) => {
|
||||||
|
|||||||
@@ -32,3 +32,4 @@ export const useAddCanvasModuleMutation = () => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
42
src/hooks/canvas/canvasNavigationHooks.tsx
Normal file
42
src/hooks/canvas/canvasNavigationHooks.tsx
Normal file
@@ -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"
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -63,4 +63,5 @@ export const canvasModuleService = {
|
|||||||
const response = await axiosClient.post<CanvasModule>(url, body);
|
const response = await axiosClient.post<CanvasModule>(url, body);
|
||||||
return response.data.id;
|
return response.data.id;
|
||||||
},
|
},
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|||||||
34
src/services/canvas/canvasNavigationService.ts
Normal file
34
src/services/canvas/canvasNavigationService.ts
Normal file
@@ -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<CanvasCourseTab[]>(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;
|
||||||
|
},
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user