import React, { useRef, useState, useEffect } from 'react'; import { Menu, Loader, Group, Text, Checkbox } from '@mantine/core'; import EditNoteIcon from '@mui/icons-material/EditNote'; import KeyboardArrowDownIcon from '@mui/icons-material/KeyboardArrowDown'; import DragIndicatorIcon from '@mui/icons-material/DragIndicator'; import { draggable, dropTargetForElements } from '@atlaskit/pragmatic-drag-and-drop/element/adapter'; import FitText from './FitText'; import { getFileColorWithOpacity } from '../pageEditor/fileColors'; import { FileId } from '../../types/file'; // Local interface for PageEditor file display interface PageEditorFile { fileId: FileId; name: string; versionNumber?: number; isSelected: boolean; } interface FileMenuItemProps { file: PageEditorFile; index: number; colorIndex: number; onToggleSelection: (fileId: FileId) => void; onReorder: (fromIndex: number, toIndex: number) => void; } const FileMenuItem: React.FC = ({ file, index, colorIndex, onToggleSelection, onReorder, }) => { const [isDragging, setIsDragging] = useState(false); const [isDragOver, setIsDragOver] = useState(false); const itemRef = useRef(null); // Keep latest values without re-registering DnD const indexRef = useRef(index); const fileIdRef = useRef(file.fileId); useEffect(() => { indexRef.current = index; }, [index]); useEffect(() => { fileIdRef.current = file.fileId; }, [file.fileId]); // NEW: keep latest onReorder without effect re-run const onReorderRef = useRef(onReorder); useEffect(() => { onReorderRef.current = onReorder; }, [onReorder]); // Gesture guard for row click vs drag const movedRef = useRef(false); const startRef = useRef<{ x: number; y: number } | null>(null); const onPointerDown = (e: React.PointerEvent) => { startRef.current = { x: e.clientX, y: e.clientY }; movedRef.current = false; }; const onPointerMove = (e: React.PointerEvent) => { if (!startRef.current) return; const dx = e.clientX - startRef.current.x; const dy = e.clientY - startRef.current.y; if (dx * dx + dy * dy > 25) movedRef.current = true; // ~5px threshold }; const onPointerUp = () => { startRef.current = null; }; useEffect(() => { const element = itemRef.current; if (!element) return; const dragCleanup = draggable({ element, getInitialData: () => ({ type: 'file-item', fileId: fileIdRef.current, fromIndex: indexRef.current, }), onDragStart: () => setIsDragging((p) => (p ? p : true)), onDrop: () => setIsDragging((p) => (p ? false : p)), canDrag: () => true, }); const dropCleanup = dropTargetForElements({ element, getData: () => ({ type: 'file-item', fileId: fileIdRef.current, toIndex: indexRef.current, }), onDragEnter: () => setIsDragOver((p) => (p ? p : true)), onDragLeave: () => setIsDragOver((p) => (p ? false : p)), onDrop: ({ source }) => { setIsDragOver(false); const sourceData = source.data as any; if (sourceData?.type === 'file-item') { const fromIndex = sourceData.fromIndex as number; const toIndex = indexRef.current; if (fromIndex !== toIndex) { onReorderRef.current(fromIndex, toIndex); // use ref, no re-register } } } }); return () => { try { dragCleanup(); } catch { // Cleanup may fail if element was already removed } try { dropCleanup(); } catch { // Cleanup may fail if element was already removed } }; }, []); // NOTE: no `onReorder` here const itemName = file?.name || 'Untitled'; const fileColorBorder = getFileColorWithOpacity(colorIndex, 1); const fileColorBorderHover = getFileColorWithOpacity(colorIndex, 1.0); return (
{ e.stopPropagation(); if (movedRef.current) return; // ignore click after drag onToggleSelection(file.fileId); }} style={{ padding: '0.75rem 0.75rem', marginBottom: '0.5rem', cursor: isDragging ? 'grabbing' : 'grab', backgroundColor: isDragOver ? 'rgba(59, 130, 246, 0.15)' : (file.isSelected ? 'rgba(0, 0, 0, 0.05)' : 'transparent'), borderLeft: `6px solid ${fileColorBorder}`, borderTop: isDragOver ? '3px solid rgb(59, 130, 246)' : 'none', borderBottom: isDragOver ? '3px solid rgb(59, 130, 246)' : 'none', opacity: isDragging ? 0.5 : 1, transition: 'opacity 0.2s ease-in-out, background-color 0.15s ease, border 0.15s ease', userSelect: 'none', }} onMouseEnter={(e) => { if (!isDragging) { (e.currentTarget as HTMLDivElement).style.backgroundColor = 'rgba(0, 0, 0, 0.05)'; (e.currentTarget as HTMLDivElement).style.borderLeftColor = fileColorBorderHover; } }} onMouseLeave={(e) => { if (!isDragging) { (e.currentTarget as HTMLDivElement).style.backgroundColor = file.isSelected ? 'rgba(0, 0, 0, 0.05)' : 'transparent'; (e.currentTarget as HTMLDivElement).style.borderLeftColor = fileColorBorder; } }} >
onToggleSelection(file.fileId)} onClick={(e) => e.stopPropagation()} size="sm" />
{file.versionNumber && file.versionNumber > 1 && ( v{file.versionNumber} )}
); }; interface PageEditorFileDropdownProps { displayName: string; files: PageEditorFile[]; onToggleSelection: (fileId: FileId) => void; onReorder: (fromIndex: number, toIndex: number) => void; switchingTo?: string | null; viewOptionStyle: React.CSSProperties; fileColorMap: Map; } export const PageEditorFileDropdown: React.FC = ({ displayName, files, onToggleSelection, onReorder, switchingTo, viewOptionStyle, fileColorMap, }) => { return (
{switchingTo === "pageEditor" ? ( ) : ( )}
{files.map((file, index) => { const colorIndex = fileColorMap.get(file.fileId as string) ?? 0; return ( ); })}
); };