Files
OpenNoodl/dev-docs/tasks/phase-2-react-migration/TASK-008-componentspanel-menus-and-sheets/TASK-008C-drag-drop-system.md

11 KiB

TASK-008C: ComponentsPanel Drag-Drop System

Overview

This subtask addresses the systematic implementation and debugging of the drag-drop system for the React-based ComponentsPanel. Previous attempts have been piecemeal, leading to circular debugging. This document provides a complete scope and test matrix.


Expected Behaviors (Full Requirements)

A. DRAG INITIATION

Requirement Description
A1 Click + hold on any component → initiates drag
A2 Click + hold on any folder → initiates drag
A3 Click + hold on any component-folder (component with nested children) → initiates drag
A4 Visual feedback: dragged item follows cursor with ghost/label
A5 Drag threshold: 5px movement before drag activates (prevents accidental drags)

B. DROP TARGETS

ID Source Target Result Example
B1 Component Component Creates nesting /PageA/PageB = /PageB/PageA
B2 Component Folder Moves into folder /MyComp/Folder/ = /Folder/MyComp
B3 Component Empty Space (Root) Moves to root level /Folder/MyComp → root = /MyComp
B4 Folder Folder Moves folder + all contents /FolderA//FolderB/ = /FolderB/FolderA/...
B5 Folder Component Nests folder inside component /FolderA//PageB = /PageB/FolderA/...
B6 Folder Empty Space (Root) Moves folder to root /Parent/FolderA/ → root = /FolderA/...
B7 Component-Folder Any target Same as folder (moves component + all children) Same as B4/B5/B6

C. VALIDATION

Requirement Description
C1 Cannot drop item onto itself
C2 Cannot drop parent into its own descendant (circular reference)
C3 Cannot create naming conflicts (same name at same level)
C4 Show "forbidden" cursor when drop not allowed

D. VISUAL FEEDBACK

Requirement Description
D1 Hover over valid target → highlight with border/background
D2 Hover over invalid target → show forbidden indicator
D3 Hover over empty space → show root drop zone indicator (blue tint)
D4 Cursor changes based on drop validity (move vs none)

E. COMPLETION

Requirement Description
E1 Successful drop → item moves, tree re-renders at new location
E2 Failed/cancelled drop → item returns to origin (no change)
E3 All operations support Undo (Cmd+Z)
E4 All operations support Redo (Cmd+Shift+Z)

Current Implementation Status

Code Inventory

File Purpose
useDragDrop.ts React hook managing drag state, uses PopupLayer.startDragging
useComponentActions.ts Drop handlers: handleDropOn(), handleDropOnRoot()
ComponentItem.tsx Drag initiation + drop target handlers for components
FolderItem.tsx Drag initiation + drop target handlers for folders
ComponentsPanelReact.tsx Background drop zone handlers
popuplayer.js Legacy jQuery drag system (startDragging, indicateDropType, endDrag)

Feature Status Matrix

Feature Handler Exists Wired Up Tested Works?
B1: Component → Component handleDropOn
B2: Component → Folder handleDropOn
B3: Component → Root handleDropOnRoot
B4: Folder → Folder handleDropOn
B5: Folder → Component handleDropOn
B6: Folder → Root handleDropOnRoot
B7: Component-Folder → any (handled as folder)

Known Issues

Issue 1: Background Drop Zone Not Triggering

  • Symptom: Dragging to empty space doesn't trigger root move
  • Likely cause: e.target === e.currentTarget check may be wrong, or handlers not attached properly
  • Debug approach: Add console.log to handleBackgroundMouseEnter

Issue 2: Nested Component → Other Component Not Working

  • Symptom: Can't drag a nested component to another component to create new nesting
  • Likely cause: canDrop validation or drop handler not triggering
  • Debug approach: Add console.log to handleDrop in ComponentItem

Issue 3: Parent Folder → Component Not Working

  • Symptom: Can't drag a folder with children onto a component
  • Likely cause: Folder→Component case may not be recognized
  • Debug approach: Check handleDropOn for folder→component case

Issue 4: Component-Folder Drag Returns to Origin

  • Symptom: Dragging component-folders snaps back instead of completing drop
  • Likely cause: Missing PopupLayer.endDrag() call or wrong case branch
  • Debug approach: Add logging to each case in handleDropOn

Implementation Plan

Phase 1: Diagnostic Logging (30 min)

Add comprehensive logging to understand current behavior:

// In ComponentItem.tsx handleMouseEnter
console.log('🎯 Component hover:', { node, isDragging: PopupLayer.instance.isDragging() });

// In FolderItem.tsx handleMouseEnter
console.log('📁 Folder hover:', { folder, isDragging: PopupLayer.instance.isDragging() });

// In ComponentsPanelReact.tsx handleBackgroundMouseEnter
console.log('🏠 Background hover:', { target: e.target, currentTarget: e.currentTarget });

// In useComponentActions.ts handleDropOn
console.log('💾 handleDropOn called:', { draggedItem, targetItem });

// In useComponentActions.ts handleDropOnRoot
console.log('🏠 handleDropOnRoot called:', { draggedItem });

Phase 2: Test Each Combination (1 hour)

Create test scenario for each combination and verify:

  1. B1: Create /CompA, /CompB. Drag /CompA onto /CompB.
  2. B2: Create /CompA, /Folder. Drag /CompA onto /Folder.
  3. B3: Create /Folder/CompA. Drag /CompA to empty space.
  4. B4: Create /FolderA, /FolderB. Drag /FolderA onto /FolderB.
  5. B5: Create /FolderA, /CompB. Drag /FolderA onto /CompB.
  6. B6: Create /Parent/FolderA. Drag /FolderA to empty space.
  7. B7: Create /CompParent with nested /CompParent/Child. Drag /CompParent onto another component.

Phase 3: Fix Issues (2-3 hours)

Address each failing combination based on diagnostic output.

Phase 4: Remove Logging & Test (30 min)

Clean up debug code and verify all combinations work.


Acceptance Criteria

All items must pass:

  • B1: Component → Component creates proper nesting
  • B2: Component → Folder moves component into folder
  • B3: Component → Root moves component to top level
  • B4: Folder → Folder moves entire folder hierarchy
  • B5: Folder → Component nests folder inside component
  • B6: Folder → Root moves folder to top level
  • B7: Component-Folder → any target works as folder
  • C1-C4: All validations prevent invalid operations
  • D1-D4: Visual feedback works for all scenarios
  • E1-E4: Completion and undo/redo work

Technical Notes

PopupLayer Drag System Integration

The legacy PopupLayer uses a jQuery-based drag system:

// Start drag
PopupLayer.instance.startDragging({
  label: 'Item Name',
  type: 'component' | 'folder',
  dragTarget: HTMLElement,
  onDragEnd: () => {
    /* cleanup */
  }
});

// During drag, from drop targets:
PopupLayer.instance.isDragging(); // Check if dragging
PopupLayer.instance.indicateDropType('move' | 'none'); // Visual feedback

// Complete drag
PopupLayer.instance.endDrag(); // Must be called for drop to complete!

Critical: If endDrag() is not called, the dragged element returns to origin.

Component-Folder Pattern

When a component has nested children (e.g., /Parent with /Parent/Child), it's rendered as a FolderItem with attached component data:

// In tree building:
{
  type: 'folder',
  data: {
    path: '/Parent',
    name: 'Parent',
    isComponentFolder: true,
    component: ComponentModel // The component at /Parent
  }
}

Drop handlers must check node.data.component to handle these properly.

Background Drop Zone

The background drop zone should trigger when:

  1. User is dragging (PopupLayer.isDragging() === true)
  2. Mouse enters the tree container
  3. Mouse is NOT over any tree item (target === currentTarget)

The current implementation uses e.target === e.currentTarget which may be too restrictive.


Files to Modify

  1. ComponentItem.tsx - Add diagnostic logging, verify drop handlers
  2. FolderItem.tsx - Add diagnostic logging, verify drop handlers
  3. ComponentsPanelReact.tsx - Fix background drop zone
  4. useDragDrop.ts - Verify canDrop logic
  5. useComponentActions.ts - Verify all drop handler cases

References

  • TASK-008 CHANGELOG - Previous fix attempts documented
  • popuplayer.js - Legacy drag system (don't modify, just understand)
  • UNDO-QUEUE-PATTERNS.md - Correct undo patterns for operations

Created: December 27, 2025 Last Updated: December 27, 2025