isolating the context menu more

This commit is contained in:
2026-02-20 09:17:19 -07:00
parent 865e86d11f
commit c224a6a9e2
4 changed files with 117 additions and 119 deletions

View File

@@ -1,5 +1,5 @@
"use client";
import { useRef, useEffect, FC, useState } from "react";
import { useEffect, FC, useState } from "react";
import { IModuleItem } from "@/features/local/modules/IModuleItem";
import { LocalAssignment } from "@/features/local/assignments/models/localAssignment";
import { useCalendarItemsContext } from "../../../context/calendarItemsContext";
@@ -15,6 +15,7 @@ import {
import { useLocalCourseSettingsQuery } from "@/features/local/course/localCoursesHooks";
import { baseCanvasUrl } from "@/features/canvas/services/canvasServiceUtils";
import { useCourseContext } from "../../../context/courseContext";
import Modal, { ModalControl } from "@/components/Modal";
function getDuplicateName(name: string, existingNames: string[]): string {
const match = name.match(/^(.*)\s+(\d+)$/);
@@ -27,15 +28,12 @@ function getDuplicateName(name: string, existingNames: string[]): string {
return `${baseName} ${num}`;
}
export const DayItemContextMenu: FC<{
x: number;
y: number;
onClose: () => void;
export const AssignmentDayItemContextMenu: FC<{
modalControl: ModalControl;
item: IModuleItem;
moduleName: string;
}> = ({ x, y, onClose, item, moduleName }) => {
}> = ({ modalControl, item, moduleName }) => {
const { courseName } = useCourseContext();
const ref = useRef<HTMLDivElement>(null);
const calendarItems = useCalendarItemsContext();
const createAssignment = useCreateAssignmentMutation();
const deleteLocal = useDeleteAssignmentMutation();
@@ -55,29 +53,21 @@ export const DayItemContextMenu: FC<{
: undefined;
useEffect(() => {
const handleClickOutside = (e: MouseEvent) => {
if (ref.current && !ref.current.contains(e.target as Node)) {
setConfirmingDelete(false);
onClose();
}
};
const handleEscape = (e: KeyboardEvent) => {
if (e.key === "Escape") {
setConfirmingDelete(false);
onClose();
modalControl.closeModal();
}
};
document.addEventListener("mousedown", handleClickOutside);
document.addEventListener("keydown", handleEscape);
return () => {
document.removeEventListener("mousedown", handleClickOutside);
document.removeEventListener("keydown", handleEscape);
};
}, [onClose]);
}, [modalControl]);
const handleClose = () => {
setConfirmingDelete(false);
onClose();
modalControl.closeModal();
};
const handleDuplicate = () => {
@@ -120,85 +110,82 @@ export const DayItemContextMenu: FC<{
}
};
const baseButtonClasses = "unstyled w-full text-left px-4 py-2";
const baseButtonClasses = "w-full text-left px-4 py-2";
const normalHoverClasses = "hover:bg-slate-700 disabled:opacity-50";
const dangerClasses =
"bg-rose-900/30 hover:bg-rose-950 disabled:opacity-50 text-rose-50";
return (
<div
ref={ref}
className="
fixed z-50 bg-slate-900 border-2 border-slate-700
rounded shadow-xl overflow-hidden
"
style={{ left: x, top: y }}
>
{confirmingDelete ? (
<Modal modalControl={modalControl}>
{() => (
<>
<button
disabled
className={`${baseButtonClasses} ${normalHoverClasses}`}
>
Delete from disk?
</button>
<button
onClick={handleDelete}
className={`${baseButtonClasses} ${dangerClasses}`}
>
Yes, delete
</button>
<button
onClick={handleClose}
className={`${baseButtonClasses} ${normalHoverClasses}`}
>
Cancel
</button>
</>
) : (
<>
{canvasUrl && (
{confirmingDelete ? (
<>
<a
href={canvasUrl}
target="_blank"
rel="noreferrer"
className="unstyled block px-4 py-2 text-sm hover:bg-slate-700 cursor-pointer"
onClick={handleClose}
>
View in Canvas
</a>
<button
onClick={handleUpdateCanvas}
disabled={updateInCanvas.isPending}
className="w-full text-left px-4 py-2 text-sm hover:bg-slate-700 disabled:opacity-50"
disabled
className={`${baseButtonClasses} ${normalHoverClasses}`}
>
Update in Canvas
Delete from disk?
</button>
<button
onClick={handleDeleteFromCanvas}
disabled={deleteFromCanvas.isPending}
onClick={handleDelete}
className={`${baseButtonClasses} ${dangerClasses}`}
>
Delete from Canvas
Yes, delete
</button>
<button
onClick={handleClose}
className={`${baseButtonClasses} ${normalHoverClasses}`}
>
Cancel
</button>
</>
) : (
<>
{canvasUrl && (
<>
<a
href={canvasUrl}
target="_blank"
rel="noreferrer"
className="unstyled block px-4 py-2 hover:bg-slate-700 cursor-pointer"
onClick={handleClose}
>
View in Canvas
</a>
<button
onClick={handleUpdateCanvas}
disabled={updateInCanvas.isPending}
className="unstyled w-full text-left px-4 py-2 hover:bg-slate-700 disabled:opacity-50"
>
Update in Canvas
</button>
<button
onClick={handleDeleteFromCanvas}
disabled={deleteFromCanvas.isPending}
className={`unstyled ${baseButtonClasses} ${dangerClasses}`}
>
Delete from Canvas
</button>
</>
)}
{!canvasUrl && (
<button
onClick={() => setConfirmingDelete(true)}
className={`unstyled ${baseButtonClasses} ${dangerClasses}`}
>
Delete from Disk
</button>
)}
<button
onClick={handleDuplicate}
className={`unstyled ${baseButtonClasses} ${normalHoverClasses}`}
>
Duplicate
</button>
</>
)}
{!canvasUrl && (
<button
onClick={() => setConfirmingDelete(true)}
className={`${baseButtonClasses} ${dangerClasses}`}
>
Delete from Disk
</button>
)}
<button
onClick={handleDuplicate}
className={`${baseButtonClasses} ${normalHoverClasses}`}
>
Duplicate
</button>
</>
)}
</div>
</Modal>
);
};

View File

@@ -2,15 +2,16 @@
import { IModuleItem } from "@/features/local/modules/IModuleItem";
import { getModuleItemUrl } from "@/services/urlUtils";
import Link from "next/link";
import { FC, ReactNode, useCallback, useState } from "react";
import { FC, ReactNode } from "react";
import { useCourseContext } from "../../../context/courseContext";
import { useTooltip } from "@/components/useTooltip";
import { DraggableItem } from "../../../context/drag/draggingContext";
import ClientOnly from "@/components/ClientOnly";
import { useDragStyleContext } from "../../../context/drag/dragStyleContext";
import { Tooltip } from "../../../../../../components/Tooltip";
import { DayItemContextMenu } from "./DayItemContextMenu";
import { AssignmentDayItemContextMenu } from "./DayItemContextMenu";
import { GetPreviewContent } from "./GetPreviewContent";
import { useModal } from "@/components/Modal";
export const ItemInDay: FC<{
type: "assignment" | "page" | "quiz";
@@ -22,23 +23,15 @@ export const ItemInDay: FC<{
const { courseName } = useCourseContext();
const { setIsDragging } = useDragStyleContext();
const { visible, targetRef, showTooltip, hideTooltip } = useTooltip(500);
const [contextMenuPos, setContextMenuPos] = useState<{
x: number;
y: number;
} | null>(null);
const modalControl = useModal();
const handleContextMenu = (e: React.MouseEvent) => {
if (type !== "assignment") return;
e.preventDefault();
e.stopPropagation();
setContextMenuPos({ x: e.clientX, y: e.clientY });
modalControl.openModal({ x: e.clientX, y: e.clientY });
};
const closeContextMenu = useCallback(() => {
setContextMenuPos(null);
}, []);
return (
<div className={" relative group "}>
<Link
@@ -87,11 +80,9 @@ export const ItemInDay: FC<{
) : (
<Tooltip message={message} targetRef={targetRef} visible={visible} />
)}
{contextMenuPos && type === "assignment" && (
<DayItemContextMenu
x={contextMenuPos.x}
y={contextMenuPos.y}
onClose={closeContextMenu}
{type === "assignment" && (
<AssignmentDayItemContextMenu
modalControl={modalControl}
item={item}
moduleName={moduleName}
/>

View File

@@ -116,7 +116,7 @@ export default function EditQuiz({
const updateQuizMutation = useUpdateQuizMutation();
const { data: globalSettings } = useGlobalSettingsQuery();
const feedbackDelimiters = getFeedbackDelimitersFromSettings(
(globalSettings ?? ({} as GlobalSettings)) as GlobalSettings
(globalSettings ?? ({} as GlobalSettings)) as GlobalSettings,
);
const { clientIsAuthoritative, text, textUpdate, monacoKey } =
@@ -141,14 +141,14 @@ export default function EditQuiz({
quizMarkdownUtils.toMarkdown(quiz, feedbackDelimiters) !==
quizMarkdownUtils.toMarkdown(
quizMarkdownUtils.parseMarkdown(text, name, feedbackDelimiters),
feedbackDelimiters
feedbackDelimiters,
)
) {
if (clientIsAuthoritative) {
const updatedQuiz = quizMarkdownUtils.parseMarkdown(
text,
quizName,
feedbackDelimiters
feedbackDelimiters,
);
await updateQuizMutation.mutateAsync({
quiz: updatedQuiz,
@@ -160,7 +160,7 @@ export default function EditQuiz({
});
} else {
console.log(
"client not authoritative, updating client with server quiz"
"client not authoritative, updating client with server quiz",
);
textUpdate(quizMarkdownUtils.toMarkdown(quiz), true);
}
@@ -178,6 +178,7 @@ export default function EditQuiz({
}, [
clientIsAuthoritative,
courseName,
feedbackDelimiters,
isFetching,
moduleName,
quiz,

View File

@@ -3,23 +3,35 @@ import React, { ReactNode, useCallback, useMemo, useState } from "react";
export interface ModalControl {
isOpen: boolean;
openModal: () => void;
openModal: (position?: { x: number; y: number }) => void;
closeModal: () => void;
position?: { x: number; y: number };
}
export function useModal() {
const [isOpen, setIsOpen] = useState(false);
const [position, setPosition] = useState<
{ x: number; y: number } | undefined
>(undefined);
const openModal = useCallback(() => setIsOpen(true), []);
const closeModal = useCallback(() => setIsOpen(false), []);
const openModal = useCallback((pos?: { x: number; y: number }) => {
setPosition(pos);
setIsOpen(true);
}, []);
const closeModal = useCallback(() => {
setIsOpen(false);
setPosition(undefined);
}, []);
return useMemo(
() => ({
isOpen,
openModal,
closeModal,
position,
}),
[closeModal, isOpen, openModal]
[closeModal, isOpen, openModal, position],
);
}
@@ -40,18 +52,21 @@ export default function Modal({
}) {
return (
<>
{buttonComponent ? (
buttonComponent({ openModal: modalControl.openModal })
) : (
<button onClick={modalControl.openModal} className={buttonClass}>
{buttonText}
</button>
)}
{buttonComponent
? buttonComponent({ openModal: () => modalControl.openModal() })
: buttonText && (
<button
onClick={() => modalControl.openModal()}
className={buttonClass}
>
{buttonText}
</button>
)}
<div
className={
modalControl.isOpen
? "transition-all duration-400 fixed inset-0 flex items-center justify-center h-screen bg-black/80 z-50 w-screen"
? `transition-all duration-400 fixed inset-0 ${modalControl.position ? "" : "flex items-center justify-center"} h-screen bg-black/80 z-50 w-screen`
: "hidden h-0 w-0 p-1 -z-50"
}
onClick={modalControl.closeModal}
@@ -60,11 +75,15 @@ export default function Modal({
onClick={(e) => {
e.stopPropagation();
}}
className={
` bg-slate-800 p-6 rounded-lg shadow-lg ` +
modalWidth +
` transition-all duration-400 ` +
` ${modalControl.isOpen ? "opacity-100" : "opacity-0"}`
className={`bg-slate-800 ${modalControl.position ? "" : "p-6"} rounded-lg shadow-lg ${modalControl.position ? "" : modalWidth} transition-all duration-400 ${modalControl.isOpen ? "opacity-100" : "opacity-0"}`}
style={
modalControl.position
? {
position: "fixed",
left: modalControl.position.x,
top: modalControl.position.y,
}
: undefined
}
>
{modalControl.isOpen &&