diff --git a/frontend/src/components/procedural-editor/StepList.tsx b/frontend/src/components/procedural-editor/StepList.tsx index f413fb48..a9cb93f1 100644 --- a/frontend/src/components/procedural-editor/StepList.tsx +++ b/frontend/src/components/procedural-editor/StepList.tsx @@ -1,5 +1,9 @@ -import { useRef, useEffect } from 'react' +import { useRef, useEffect, useCallback, type ReactNode } from 'react' import { Plus, GripVertical, Trash2, ChevronDown, CheckCircle2, AlertTriangle, Info, Zap, Shield, SeparatorHorizontal } from 'lucide-react' +import { DndContext, closestCenter, KeyboardSensor, PointerSensor, useSensor, useSensors } from '@dnd-kit/core' +import type { DragEndEvent } from '@dnd-kit/core' +import { SortableContext, sortableKeyboardCoordinates, verticalListSortingStrategy, useSortable } from '@dnd-kit/sortable' +import { CSS } from '@dnd-kit/utilities' import type { StepContentType } from '@/types' import { StepEditor } from './StepEditor' import { useProceduralEditorStore } from '@/store/proceduralEditorStore' @@ -12,6 +16,29 @@ const contentTypeConfig: Record }) => ReactNode +}) { + const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({ id, disabled }) + + const style = { + transform: CSS.Transform.toString(transform), + transition, + } + + return ( +
+ {children({ dragHandleProps: { ...attributes, ...listeners } })} +
+ ) +} + export function StepList() { const { steps, @@ -22,6 +49,7 @@ export function StepList() { addSectionHeader, removeStep, updateStep, + moveStep, } = useProceduralEditorStore() const procedureSteps = steps.filter((s) => s.type === 'procedure_step') @@ -35,7 +63,6 @@ export function StepList() { useEffect(() => { if (steps.length > prevStepCount.current) { - // Scroll the newly expanded step into view setTimeout(() => { scrollTargetRef.current?.scrollIntoView({ behavior: 'smooth', block: 'nearest' }) }, 50) @@ -43,6 +70,30 @@ export function StepList() { prevStepCount.current = steps.length }, [steps.length]) + // DnD setup + const sensors = useSensors( + useSensor(PointerSensor, { activationConstraint: { distance: 8 } }), + useSensor(KeyboardSensor, { coordinateGetter: sortableKeyboardCoordinates }) + ) + + const handleDragEnd = useCallback((event: DragEndEvent) => { + const { active, over } = event + if (!over || active.id === over.id) return + + const oldIndex = steps.findIndex(s => s.id === active.id) + const newIndex = steps.findIndex(s => s.id === over.id) + if (oldIndex === -1 || newIndex === -1) return + + // Don't allow moving past the procedure_end step + const endIndex = steps.findIndex(s => s.type === 'procedure_end') + if (newIndex >= endIndex) return + + moveStep(oldIndex, newIndex) + }, [steps, moveStep]) + + // Sortable items: everything except procedure_end + const sortableItems = steps.filter(s => s.type !== 'procedure_end').map(s => s.id) + let stepCounter = 0 return ( @@ -74,138 +125,168 @@ export function StepList() { -
- {steps.map((step) => { - if (step.type === 'procedure_end') { - return ( -
- - updateStep(step.id, { title: e.target.value })} - className="flex-1 bg-transparent text-sm text-muted-foreground focus:outline-none" - placeholder="Procedure Complete" - /> - END -
- ) - } + + +
+ {steps.map((step) => { + if (step.type === 'procedure_end') { + // procedure_end: non-draggable, always last + return ( +
+ + updateStep(step.id, { title: e.target.value })} + className="flex-1 bg-transparent text-sm text-muted-foreground focus:outline-none" + placeholder="Procedure Complete" + /> + END +
+ ) + } - // Section header rendering - if (step.type === 'section_header') { - const isExpanded = expandedStepId === step.id + // Section header rendering + if (step.type === 'section_header') { + const isExpanded = expandedStepId === step.id + + if (isExpanded) { + return ( + + {() => ( +
+ updateStep(step.id, updates)} + onCollapse={() => setExpandedStepId(null)} + availableVariables={intakeForm} + /> +
+ )} +
+ ) + } + + return ( + + {({ dragHandleProps }) => ( +
+ + setExpandedStepId(step.id)} + > + {step.title || 'Untitled Section'} + + +
+ )} +
+ ) + } + + // Regular procedure step + stepCounter++ + const stepNumber = stepCounter + const isExpanded = expandedStepId === step.id + const contentType = step.content_type || 'action' + const config = contentTypeConfig[contentType] + const Icon = config.icon + + if (isExpanded) { + return ( + + {() => ( +
+ updateStep(step.id, updates)} + onCollapse={() => setExpandedStepId(null)} + availableVariables={intakeForm} + /> +
+ )} +
+ ) + } - if (isExpanded) { return ( -
- updateStep(step.id, updates)} - onCollapse={() => setExpandedStepId(null)} - availableVariables={intakeForm} - /> -
+ + {({ dragHandleProps }) => ( +
+ + + + {stepNumber} + + + + + + + setExpandedStepId(step.id)} + > + {step.title || 'Untitled step'} + + + {step.estimated_minutes && ( + + ~{step.estimated_minutes}m + + )} + + + + +
+ )} +
) - } - - return ( -
- - setExpandedStepId(step.id)} - > - {step.title || 'Untitled Section'} - - -
- ) - } - - // Regular procedure step - stepCounter++ - const stepNumber = stepCounter - const isExpanded = expandedStepId === step.id - const contentType = step.content_type || 'action' - const config = contentTypeConfig[contentType] - const Icon = config.icon - - if (isExpanded) { - return ( -
- updateStep(step.id, updates)} - onCollapse={() => setExpandedStepId(null)} - availableVariables={intakeForm} - /> -
- ) - } - - return ( -
-
- - - - {stepNumber} - - - - - - - setExpandedStepId(step.id)} - > - {step.title || 'Untitled step'} - - - {step.estimated_minutes && ( - - ~{step.estimated_minutes}m - - )} - - - - -
-
- ) - })} -
+ })} +
+ + {/* Add step button at bottom */}