mirror of
https://github.com/alexmickelson/canvasManagement.git
synced 2026-03-25 23:28:33 -06:00
Compare commits
4 Commits
9ce42c21f9
...
b62dcb9c62
| Author | SHA1 | Date | |
|---|---|---|---|
| b62dcb9c62 | |||
|
|
77fde8198e | ||
|
|
8172724a4f | ||
|
|
07155991aa |
@@ -1,7 +1,8 @@
|
|||||||
|
"use client";
|
||||||
import { IModuleItem } from "@/features/local/modules/IModuleItem";
|
import { IModuleItem } from "@/features/local/modules/IModuleItem";
|
||||||
import { getModuleItemUrl } from "@/services/urlUtils";
|
import { getModuleItemUrl } from "@/services/urlUtils";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { ReactNode } from "react";
|
import { ReactNode, useCallback, useState } from "react";
|
||||||
import { useCourseContext } from "../../context/courseContext";
|
import { useCourseContext } from "../../context/courseContext";
|
||||||
import { useTooltip } from "@/components/useTooltip";
|
import { useTooltip } from "@/components/useTooltip";
|
||||||
import MarkdownDisplay from "@/components/MarkdownDisplay";
|
import MarkdownDisplay from "@/components/MarkdownDisplay";
|
||||||
@@ -9,6 +10,31 @@ import { DraggableItem } from "../../context/drag/draggingContext";
|
|||||||
import ClientOnly from "@/components/ClientOnly";
|
import ClientOnly from "@/components/ClientOnly";
|
||||||
import { useDragStyleContext } from "../../context/drag/dragStyleContext";
|
import { useDragStyleContext } from "../../context/drag/dragStyleContext";
|
||||||
import { Tooltip } from "../../../../../components/Tooltip";
|
import { Tooltip } from "../../../../../components/Tooltip";
|
||||||
|
import { ContextMenu, ContextMenuItem } from "@/components/ContextMenu";
|
||||||
|
import {
|
||||||
|
useCanvasAssignmentsQuery,
|
||||||
|
useUpdateAssignmentInCanvasMutation,
|
||||||
|
useDeleteAssignmentFromCanvasMutation,
|
||||||
|
} from "@/features/canvas/hooks/canvasAssignmentHooks";
|
||||||
|
import { useLocalCourseSettingsQuery } from "@/features/local/course/localCoursesHooks";
|
||||||
|
import { baseCanvasUrl } from "@/features/canvas/services/canvasServiceUtils";
|
||||||
|
import {
|
||||||
|
useDeleteAssignmentMutation,
|
||||||
|
useCreateAssignmentMutation,
|
||||||
|
} from "@/features/local/assignments/assignmentHooks";
|
||||||
|
import { LocalAssignment } from "@/features/local/assignments/models/localAssignment";
|
||||||
|
import { useCalendarItemsContext } from "../../context/calendarItemsContext";
|
||||||
|
|
||||||
|
function getDuplicateName(name: string, existingNames: string[]): string {
|
||||||
|
const match = name.match(/^(.*)\s+(\d+)$/);
|
||||||
|
const baseName = match ? match[1] : name;
|
||||||
|
const startNum = match ? parseInt(match[2]) + 1 : 2;
|
||||||
|
let num = startNum;
|
||||||
|
while (existingNames.includes(`${baseName} ${num}`)) {
|
||||||
|
num++;
|
||||||
|
}
|
||||||
|
return `${baseName} ${num}`;
|
||||||
|
}
|
||||||
|
|
||||||
function getPreviewContent(
|
function getPreviewContent(
|
||||||
type: "assignment" | "page" | "quiz",
|
type: "assignment" | "page" | "quiz",
|
||||||
@@ -60,6 +86,114 @@ export function ItemInDay({
|
|||||||
const { setIsDragging } = useDragStyleContext();
|
const { setIsDragging } = useDragStyleContext();
|
||||||
const { visible, targetRef, showTooltip, hideTooltip } = useTooltip(500);
|
const { visible, targetRef, showTooltip, hideTooltip } = useTooltip(500);
|
||||||
|
|
||||||
|
const [contextMenuPos, setContextMenuPos] = useState<{
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
} | null>(null);
|
||||||
|
const [confirmingDelete, setConfirmingDelete] = useState(false);
|
||||||
|
|
||||||
|
const { data: canvasAssignments } = useCanvasAssignmentsQuery();
|
||||||
|
const { data: settings } = useLocalCourseSettingsQuery();
|
||||||
|
const updateInCanvas = useUpdateAssignmentInCanvasMutation();
|
||||||
|
const deleteFromCanvas = useDeleteAssignmentFromCanvasMutation();
|
||||||
|
const deleteLocal = useDeleteAssignmentMutation();
|
||||||
|
const createAssignment = useCreateAssignmentMutation();
|
||||||
|
const calendarItems = useCalendarItemsContext();
|
||||||
|
|
||||||
|
const assignmentInCanvas =
|
||||||
|
type === "assignment"
|
||||||
|
? canvasAssignments?.find((a) => a.name === item.name)
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
const handleContextMenu = (e: React.MouseEvent) => {
|
||||||
|
if (type !== "assignment") return;
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
setContextMenuPos({ x: e.clientX, y: e.clientY });
|
||||||
|
setConfirmingDelete(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const closeContextMenu = useCallback(() => {
|
||||||
|
setContextMenuPos(null);
|
||||||
|
setConfirmingDelete(false);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleDuplicate = useCallback(() => {
|
||||||
|
const assignment = item as LocalAssignment;
|
||||||
|
const existingNames = Object.values(calendarItems).flatMap((modules) =>
|
||||||
|
(modules[moduleName]?.assignments ?? []).map((a) => a.name)
|
||||||
|
);
|
||||||
|
const newName = getDuplicateName(item.name, existingNames);
|
||||||
|
createAssignment.mutate({
|
||||||
|
courseName,
|
||||||
|
moduleName,
|
||||||
|
assignmentName: newName,
|
||||||
|
assignment: { ...assignment, name: newName },
|
||||||
|
});
|
||||||
|
closeContextMenu();
|
||||||
|
}, [
|
||||||
|
item,
|
||||||
|
calendarItems,
|
||||||
|
moduleName,
|
||||||
|
createAssignment,
|
||||||
|
courseName,
|
||||||
|
closeContextMenu,
|
||||||
|
]);
|
||||||
|
|
||||||
|
const contextMenuItems: ContextMenuItem[] = confirmingDelete
|
||||||
|
? [
|
||||||
|
{ label: "Delete from disk?", disabled: true },
|
||||||
|
{
|
||||||
|
label: "Yes, delete",
|
||||||
|
variant: "danger",
|
||||||
|
onClick: () => {
|
||||||
|
deleteLocal.mutate({ courseName, moduleName, assignmentName: item.name });
|
||||||
|
closeContextMenu();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{ label: "Cancel", onClick: closeContextMenu },
|
||||||
|
]
|
||||||
|
: [
|
||||||
|
...(assignmentInCanvas
|
||||||
|
? [
|
||||||
|
{
|
||||||
|
label: "View in Canvas",
|
||||||
|
href: `${baseCanvasUrl}/courses/${settings.canvasId}/assignments/${assignmentInCanvas.id}`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Update in Canvas",
|
||||||
|
onClick: () => {
|
||||||
|
updateInCanvas.mutate({
|
||||||
|
canvasAssignmentId: assignmentInCanvas.id,
|
||||||
|
assignment: item as LocalAssignment,
|
||||||
|
});
|
||||||
|
closeContextMenu();
|
||||||
|
},
|
||||||
|
disabled: updateInCanvas.isPending,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Delete from Canvas",
|
||||||
|
variant: "danger" as const,
|
||||||
|
onClick: () => {
|
||||||
|
deleteFromCanvas.mutate({
|
||||||
|
canvasAssignmentId: assignmentInCanvas.id,
|
||||||
|
assignmentName: item.name,
|
||||||
|
});
|
||||||
|
closeContextMenu();
|
||||||
|
},
|
||||||
|
disabled: deleteFromCanvas.isPending,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
: [
|
||||||
|
{
|
||||||
|
label: "Delete from Disk",
|
||||||
|
variant: "danger" as const,
|
||||||
|
onClick: () => setConfirmingDelete(true),
|
||||||
|
},
|
||||||
|
]),
|
||||||
|
{ label: "Duplicate", onClick: handleDuplicate },
|
||||||
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={" relative group "}>
|
<div className={" relative group "}>
|
||||||
<Link
|
<Link
|
||||||
@@ -89,6 +223,7 @@ export function ItemInDay({
|
|||||||
}}
|
}}
|
||||||
onMouseEnter={showTooltip}
|
onMouseEnter={showTooltip}
|
||||||
onMouseLeave={hideTooltip}
|
onMouseLeave={hideTooltip}
|
||||||
|
onContextMenu={handleContextMenu}
|
||||||
ref={targetRef}
|
ref={targetRef}
|
||||||
>
|
>
|
||||||
{item.name}
|
{item.name}
|
||||||
@@ -107,6 +242,14 @@ export function ItemInDay({
|
|||||||
) : (
|
) : (
|
||||||
<Tooltip message={message} targetRef={targetRef} visible={visible} />
|
<Tooltip message={message} targetRef={targetRef} visible={visible} />
|
||||||
)}
|
)}
|
||||||
|
{contextMenuPos && type === "assignment" && (
|
||||||
|
<ContextMenu
|
||||||
|
x={contextMenuPos.x}
|
||||||
|
y={contextMenuPos.y}
|
||||||
|
items={contextMenuItems}
|
||||||
|
onClose={closeContextMenu}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</ClientOnly>
|
</ClientOnly>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -75,15 +75,14 @@ export default function AssignmentPreview({
|
|||||||
{extraPoints !== 0 && (
|
{extraPoints !== 0 && (
|
||||||
<h5 className="text-center">{extraPoints} Extra Credit Points</h5>
|
<h5 className="text-center">{extraPoints} Extra Credit Points</h5>
|
||||||
)}
|
)}
|
||||||
<div className="grid grid-cols-3">
|
<div className="grid grid-cols-4">
|
||||||
{assignment.rubric.map((rubricItem, i) => (
|
{assignment.rubric.map((rubricItem, i) => (
|
||||||
<Fragment key={rubricItem.label + i}>
|
<Fragment key={rubricItem.label + i}>
|
||||||
<div className="text-end pe-3 col-span-2">{rubricItem.label}</div>
|
<div className="text-end pe-1">
|
||||||
<div>
|
{rubricItemIsExtraCredit(rubricItem) ? "Extra Credit" : ""}
|
||||||
{rubricItem.points}
|
|
||||||
|
|
||||||
{rubricItemIsExtraCredit(rubricItem) ? " - Extra Credit" : ""}
|
|
||||||
</div>
|
</div>
|
||||||
|
<div className="text-end pe-3">{rubricItem.points}</div>
|
||||||
|
<div className="col-span-2">{rubricItem.label}</div>
|
||||||
</Fragment>
|
</Fragment>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
78
src/components/ContextMenu.tsx
Normal file
78
src/components/ContextMenu.tsx
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
"use client";
|
||||||
|
import { useEffect, useRef } from "react";
|
||||||
|
|
||||||
|
export interface ContextMenuItem {
|
||||||
|
label: string;
|
||||||
|
onClick?: () => void;
|
||||||
|
href?: string;
|
||||||
|
variant?: "default" | "danger";
|
||||||
|
disabled?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ContextMenu({
|
||||||
|
x,
|
||||||
|
y,
|
||||||
|
items,
|
||||||
|
onClose,
|
||||||
|
}: {
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
items: ContextMenuItem[];
|
||||||
|
onClose: () => void;
|
||||||
|
}) {
|
||||||
|
const ref = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const handleClickOutside = (e: MouseEvent) => {
|
||||||
|
if (ref.current && !ref.current.contains(e.target as Node)) {
|
||||||
|
onClose();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const handleEscape = (e: KeyboardEvent) => {
|
||||||
|
if (e.key === "Escape") onClose();
|
||||||
|
};
|
||||||
|
document.addEventListener("mousedown", handleClickOutside);
|
||||||
|
document.addEventListener("keydown", handleEscape);
|
||||||
|
return () => {
|
||||||
|
document.removeEventListener("mousedown", handleClickOutside);
|
||||||
|
document.removeEventListener("keydown", handleEscape);
|
||||||
|
};
|
||||||
|
}, [onClose]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
ref={ref}
|
||||||
|
className="fixed z-50 bg-slate-800 border border-slate-600 rounded shadow-xl overflow-hidden min-w-44"
|
||||||
|
style={{ left: x, top: y }}
|
||||||
|
>
|
||||||
|
{items.map((item, i) =>
|
||||||
|
item.href ? (
|
||||||
|
<a
|
||||||
|
key={i}
|
||||||
|
href={item.href}
|
||||||
|
target="_blank"
|
||||||
|
rel="noreferrer"
|
||||||
|
className="block px-4 py-2 text-sm hover:bg-slate-700 cursor-pointer whitespace-nowrap"
|
||||||
|
onClick={onClose}
|
||||||
|
>
|
||||||
|
{item.label}
|
||||||
|
</a>
|
||||||
|
) : (
|
||||||
|
<button
|
||||||
|
key={i}
|
||||||
|
onClick={item.onClick}
|
||||||
|
disabled={item.disabled}
|
||||||
|
className={
|
||||||
|
"w-full text-left px-4 py-2 text-sm hover:bg-slate-700 disabled:opacity-50 whitespace-nowrap " +
|
||||||
|
(item.variant === "danger"
|
||||||
|
? "text-red-400 hover:text-red-300"
|
||||||
|
: "")
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{item.label}
|
||||||
|
</button>
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user