mirror of
https://github.com/alexmickelson/canvasManagement.git
synced 2026-03-25 15:18:32 -06:00
isolating the context menu more
This commit is contained in:
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 &&
|
||||
|
||||
Reference in New Issue
Block a user