can manage course navigation from settinss

This commit is contained in:
2025-07-07 11:06:04 -06:00
parent d8f17faaae
commit 5a56d26b4d
11 changed files with 264 additions and 17 deletions

View File

@@ -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() {
<DefaultDueTime />
<AssignmentGroupManagement />
<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>
</>
);
}

View File

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

View File

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

View File

@@ -14,7 +14,7 @@ export function makeQueryClient() {
// refetchInterval: 7000, // debug only
refetchOnWindowFocus: false,
retry: 0,
refetchOnMount: false,
// refetchOnMount: false,
},
mutations: {
onError: (error) => {

View File

@@ -32,3 +32,4 @@ export const useAddCanvasModuleMutation = () => {
},
});
};

View 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"
});
},
});
};

View File

@@ -63,4 +63,5 @@ export const canvasModuleService = {
const response = await axiosClient.post<CanvasModule>(url, body);
return response.data.id;
},
};

View 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;
},
};