All files / src/pages/VisProgPage/visualProgrammingUI/components DragDropSidebar.tsx

0% Statements 0/25
0% Branches 0/9
0% Functions 0/9
0% Lines 0/25

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160                                                                                                                                                                                                                                                                                                                               
import { useDraggable } from '@neodrag/react';
import { useReactFlow, type XYPosition } from '@xyflow/react';
import { type ReactNode, useCallback, useRef, useState } from 'react';
import useFlowStore from '../VisProgStores';
import styles from '../../VisProg.module.css';
import { NodeDefaults, type NodeTypes} from '../NodeRegistry'
import {Tooltip} from "./NodeComponents.tsx";
 
/**
 * Props for a draggable node within the drag-and-drop toolbar.
 *
 * @property className - Optional custom CSS classes for styling.
 * @property children - The visual content or label rendered inside the draggable node.
 * @property nodeType - The type of node represented (key from `NodeTypes`).
 * @property onDrop - Function called when the node is dropped on the flow pane.
 */
interface DraggableNodeProps {
  className?: string;
  children: ReactNode;
  nodeType: keyof typeof NodeTypes;
  onDrop: (nodeType: keyof typeof NodeTypes, position: XYPosition) => void;
}
 
/**
 * A draggable node element used in the drag-and-drop toolbar.
 *
 * Integrates with the NeoDrag library to handle drag events.
 * On drop, it calls the provided `onDrop` function with the node type and drop position.
 *
 * @param props - The draggable node configuration.
 * @returns A React element representing a draggable node.
 */
function DraggableNode({ className, children, nodeType, onDrop }: DraggableNodeProps) {
  const draggableRef = useRef<HTMLDivElement>(null);
  const [position, setPosition] = useState<XYPosition>({ x: 0, y: 0 });
 
  // The NeoDrag hook enables smooth drag functionality for this element.
  // @ts-expect-error: NeoDrag typing incompatibility — safe to ignore.
  useDraggable(draggableRef, {
    position,
    onDrag: ({ offsetX, offsetY }) => {
      setPosition({ x: offsetX, y: offsetY });
    },
    onDragEnd: ({ event }) => {
      setPosition({ x: 0, y: 0 });
      onDrop(nodeType, { x: event.clientX, y: event.clientY });
    },
  });
 
  return (
    <Tooltip nodeType={nodeType}>
      <div>
        <div className={className}
             ref={draggableRef}
             id={`draggable-${nodeType}`}
             data-testid={`draggable-${nodeType}`}
        >
          {children}
        </div>
      </div>
    </Tooltip>)
}
 
/**
 * Adds a new node to the flow graph.
 *
 * Handles:
 * - Automatic node ID generation based on existing nodes of the same type.
 * - Loading of default data from the `NodeDefaults` registry.
 * - Integration with the flow store to update global node state.
 *
 * @param nodeType - The type of node to create (from `NodeTypes`).
 * @param position - The XY position in the flow canvas where the node will appear.
 */
function addNodeToFlow(nodeType: keyof typeof NodeTypes, position: XYPosition) {
  const { addNode } = useFlowStore.getState();
 
  // Load any predefined data for this node type.
  const defaultData = NodeDefaults[nodeType] ?? {}
  const id = crypto.randomUUID();
  
  // Create new node
  const newNode = {
    id: id,
    type: nodeType,
    position,
    data: JSON.parse(JSON.stringify(defaultData))
  }
  addNode(newNode);
}
 
/**
 * The drag-and-drop toolbar component for the visual programming interface.
 *
 * Displays draggable node templates based on entries in `NodeDefaults`.
 * Each droppable node can be dragged into the flow pane to instantiate it.
 *
 * Automatically filters nodes whose `droppable` flag is set to `true`.
 *
 * @returns A React element representing the drag-and-drop toolbar.
 */
export function DndToolbar() {
  const { screenToFlowPosition } = useReactFlow();
 
  /**
   * Handles dropping a node onto the flow pane.
   * Translates screen coordinates into flow coordinates using React Flow utilities.
   */
  const handleNodeDrop = useCallback(
    (nodeType: keyof typeof NodeTypes, screenPosition: XYPosition) => {
      const flow = document.querySelector('.react-flow');
      const flowRect = flow?.getBoundingClientRect();
      
      // Only add the node if it is inside the flow canvas area.
      const isInFlow =
        flowRect &&
        screenPosition.x >= flowRect.left &&
        screenPosition.x <= flowRect.right &&
        screenPosition.y >= flowRect.top &&
        screenPosition.y <= flowRect.bottom;
 
      if (isInFlow) {
        const position = screenToFlowPosition(screenPosition);
        addNodeToFlow(nodeType, position);
      }
    },
    [screenToFlowPosition],
  );
 
 
  // Map over the default nodes to get all nodes that can be dropped from the toolbar.
  const droppableNodes = Object.entries(NodeDefaults)
  .filter(([, data]) => data.droppable)
  .map(([type, data]) => ({ 
    type: type as DraggableNodeProps['nodeType'], 
    data 
  }));
 
  return (
    <div className={`flex-col gap-lg padding-md ${styles.innerDndPanel}`} id={"draggable-sidebar"}>
      <div className="description">
        You can drag these nodes to the pane to create new nodes.
      </div>
      <div className={`flex-row gap-lg ${styles.dndNodeContainer}`}>
        {/* Maps over all the nodes that are droppable, and puts them in the panel */}
        {droppableNodes.map(({type, data}) => (
          <DraggableNode
            key={type}
            className={styles[`draggable-node-${type}`]} // Our current style signature for nodes
            nodeType={type}
            onDrop={handleNodeDrop}
          >
            {data.label}
          </DraggableNode>
        ))}
      </div>
    </div>
  );
}