import { useState, useCallback } from "react"; import { Plus, Pencil, Trash2, Check, X, GripVertical } from "lucide-react"; import type { ToastType } from "../hooks/useToast"; import { useRoadmaps } from "../hooks/useRoadmaps"; import type { Roadmap, RoadmapMilestone, RoadmapFeature, RoadmapCreateInput, RoadmapUpdateInput, RoadmapMilestoneCreateInput, RoadmapMilestoneUpdateInput, RoadmapFeatureCreateInput, RoadmapFeatureUpdateInput, } from "@fusion/core"; export interface RoadmapsViewProps { projectId?: string; addToast: (message: string, type?: ToastType) => void; } // ── Drag State Types ──────────────────────────────────────────────── interface MilestoneDragState { draggingId: string | null; dropTargetId: string | null; dropPosition: "before" | "after" | null; } interface FeatureDragState { draggingId: string | null; draggingMilestoneId: string | null; dropTargetMilestoneId: string | null; dropTargetIndex: number | null; dropPosition: "before" | "after" | null; } // ── Inline Edit State Types ───────────────────────────────────────── interface InlineEditState { roadmapId: string | null; field: "title" | "description" | null; value: string; } interface MilestoneInlineEditState { milestoneId: string | null; field: "title" | "description" | null; value: string; } interface FeatureInlineEditState { featureId: string | null; field: "title" | "description" | null; value: string; } // ── Create Form State ─────────────────────────────────────────────── interface CreateFormState { type: "roadmap" | "milestone" | "feature" | null; parentId?: string; title: string; description: string; } // ── Roadmap Item ───────────────────────────────────────────────────── function RoadmapItem({ roadmap, isSelected, onSelect, onEdit, onDelete, }: { roadmap: Roadmap; isSelected: boolean; onSelect: () => void; onEdit: () => void; onDelete: () => void; }) { const handleKeyDown = (e: React.KeyboardEvent) => { if (e.key === "Enter") { onSelect(); } }; const handleEditClick = (e: React.MouseEvent) => { e.stopPropagation(); onEdit(); }; const handleDeleteClick = (e: React.MouseEvent) => { e.stopPropagation(); onDelete(); }; return (
{roadmap.title}
{roadmap.description && (
{roadmap.description}
)}
); } // ── Milestone Card ─────────────────────────────────────────────────── function MilestoneCard({ milestone, features, onEditMilestone, onDeleteMilestone, onAddFeature, onEditFeature, onDeleteFeature, milestoneEdit, onStartMilestoneEdit, onCancelMilestoneEdit, onSaveMilestoneEdit, featureEdit, onStartFeatureEdit, onCancelFeatureEdit, onSaveFeatureEdit, projectId, addToast, // Milestone drag-and-drop props isMilestoneDragging, isMilestoneDropTarget, milestoneDropPosition, onMilestoneDragStart, onMilestoneDragEnd, onMilestoneDragOver, onMilestoneDrop, onMilestoneDragLeave, // Feature drag-and-drop props isFeatureDragging, isFeatureDropTarget, featureDropIndex, onFeatureDragStart, onFeatureDragEnd, onFeatureDragOver, onFeatureDrop, onFeatureDragLeave, onFeatureDropOnMilestone, }: { milestone: RoadmapMilestone; features: RoadmapFeature[]; onEditMilestone: () => void; onDeleteMilestone: () => void; onAddFeature: () => void; onEditFeature: (featureId: string) => void; onDeleteFeature: (featureId: string) => void; milestoneEdit: MilestoneInlineEditState | null; onStartMilestoneEdit: () => void; onCancelMilestoneEdit: () => void; onSaveMilestoneEdit: (updates: RoadmapMilestoneUpdateInput) => void; featureEdit: FeatureInlineEditState | null; onStartFeatureEdit: (featureId: string, currentTitle: string, currentDescription?: string) => void; onCancelFeatureEdit: () => void; onSaveFeatureEdit: (updates: RoadmapFeatureUpdateInput) => void; projectId?: string; addToast: (message: string, type?: ToastType) => void; // Milestone drag-and-drop props isMilestoneDragging: boolean; isMilestoneDropTarget: boolean; milestoneDropPosition: "before" | "after" | null; onMilestoneDragStart: (milestoneId: string) => void; onMilestoneDragEnd: () => void; onMilestoneDragOver: (milestoneId: string) => void; onMilestoneDrop: (milestoneId: string) => void; onMilestoneDragLeave: (e: React.DragEvent) => void; // Feature drag-and-drop props isFeatureDragging: (featureId: string) => boolean; isFeatureDropTarget: boolean; featureDropIndex: number | null; onFeatureDragStart: (featureId: string, milestoneId: string) => void; onFeatureDragEnd: () => void; onFeatureDragOver: (featureId: string, position: "before" | "after") => void; onFeatureDrop: (featureId: string, targetIndex: number) => void; onFeatureDragLeave: (e: React.DragEvent) => void; onFeatureDropOnMilestone: () => void; }) { const isEditingMilestone = milestoneEdit?.milestoneId === milestone.id; const handleMilestoneTitleKeyDown = (e: React.KeyboardEvent) => { if (e.key === "Enter") { e.preventDefault(); if (milestoneEdit) { onSaveMilestoneEdit({ title: milestoneEdit.value }); } } else if (e.key === "Escape") { onCancelMilestoneEdit(); } }; const handleMilestoneDescKeyDown = (e: React.KeyboardEvent) => { if (e.key === "Escape") { onCancelMilestoneEdit(); } }; // Build class names for drag states const milestoneClasses = [ "roadmaps-view__milestone", isMilestoneDragging ? "roadmaps-view__milestone--dragging" : "", isMilestoneDropTarget ? "roadmaps-view__milestone--drop-target" : "", isMilestoneDropTarget && milestoneDropPosition === "before" ? "roadmaps-view__milestone--drop-before" : "", isMilestoneDropTarget && milestoneDropPosition === "after" ? "roadmaps-view__milestone--drop-after" : "", ].filter(Boolean).join(" "); // Build class names for feature list drop state const featureListClasses = [ "roadmaps-view__feature-list", isFeatureDropTarget ? "roadmaps-view__feature-list--drop-target" : "", ].filter(Boolean).join(" "); return (
{ if (!isEditingMilestone) { onMilestoneDragStart(milestone.id); e.dataTransfer.setData("text/plain", `milestone:${milestone.id}`); e.dataTransfer.effectAllowed = "move"; } }} onDragEnd={onMilestoneDragEnd} onDragOver={(e) => { // Only prevent default for milestone drops, not feature drops if (e.dataTransfer.types.includes("text/plain")) { const data = e.dataTransfer.types.includes("text/plain"); if (data) { // This is a milestone drag e.preventDefault(); e.dataTransfer.dropEffect = "move"; onMilestoneDragOver(milestone.id); } } }} onDrop={(e) => { e.preventDefault(); // Check if this is a feature drop or milestone drop const data = e.dataTransfer.getData("text/plain"); if (data?.startsWith("feature:")) { // Feature drop - handled by child element } else { onMilestoneDrop(milestone.id); } }} onDragLeave={onMilestoneDragLeave} data-testid={`milestone-card-${milestone.id}`} >
{isEditingMilestone ? (
onStartMilestoneEdit() } onKeyDown={handleMilestoneTitleKeyDown} placeholder="Milestone title" autoFocus data-testid={`milestone-title-input-${milestone.id}`} />