feat: draggable

This commit is contained in:
scarqin
2024-12-23 18:16:17 +08:00
parent 23c8a84b4c
commit cbce30b4d7
2 changed files with 113 additions and 4 deletions
@@ -1,12 +1,13 @@
import { Icon } from '@iconify/react'
import { Handle, Position } from '@xyflow/react'
import React from 'react'
import React, { useCallback, useState } from 'react'
import { ModelCardStatus } from './types'
interface ModelCardData {
title: string
status: ModelCardStatus
defaultModel: string
onDragStart?: () => void
}
type ModelCardNodeData = ModelCardData & {
@@ -15,11 +16,49 @@ type ModelCardNodeData = ModelCardData & {
}
export const ModelCardNode: React.FC<{ data: ModelCardNodeData }> = ({ data }) => {
const [isHovered, setIsHovered] = useState(false)
const { title, status, defaultModel } = data
const onDragHandleMouseDown = useCallback((event: React.MouseEvent) => {
// Prevent event propagation to allow dragging
event.stopPropagation()
// Create a new drag event
const dragEvent = new MouseEvent('mousedown', {
clientX: event.clientX,
clientY: event.clientY,
bubbles: true
})
// Find the node element and dispatch the event
const nodeElement = event.currentTarget.closest('.react-flow__node')
if (nodeElement) {
// Use the global `document` object if it exists
nodeElement.dispatchEvent(dragEvent)
}
}, [])
return (
<div className="node-card bg-white rounded-lg shadow-sm p-4 min-w-[280px] relative">
<div
className="node-card bg-white rounded-lg shadow-sm p-4 min-w-[280px] relative group nodrag"
onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)}
>
<Handle type="target" position={Position.Left} />
<Handle type="source" position={Position.Right} />
{/* Drag Handle - Only visible on hover */}
<div
className={`absolute left-0 top-1/2 -translate-y-1/2 -translate-x-full transition-opacity duration-200
${isHovered ? 'opacity-100' : 'opacity-0'}`}
>
<div
className="w-6 h-10 flex items-center justify-center cursor-grab bg-white rounded-l-md border border-r-0 border-gray-200 hover:border-[--primary-color] hover:text-[--primary-color]"
onMouseDown={onDragHandleMouseDown}
>
<Icon icon="mdi:drag" className="text-xl" />
</div>
</div>
<div className="flex justify-between items-center">
<div className="flex gap-2 items-center">
<Icon icon="mdi:robot" className="text-xl text-[--primary-color]" />
@@ -29,7 +68,15 @@ export const ModelCardNode: React.FC<{ data: ModelCardNodeData }> = ({ data }) =
className={`text-xl ${status === 'success' ? 'text-green-500' : 'text-red-500'}`}
/>
</div>
<Icon icon="mdi:cog" className="text-xl text-gray-400 cursor-pointer hover:text-[--primary-color]" />
{/* Action buttons */}
<div className="flex gap-2 transition-opacity duration-200">
<Icon
icon="mdi:cog"
className="text-xl text-gray-400 cursor-pointer hover:text-[--primary-color]"
onClick={() => console.log('Settings', data.id)}
/>
</div>
</div>
<div className="mt-2 text-sm text-gray-500">{defaultModel}</div>
</div>
@@ -213,6 +213,66 @@ const Playground = () => {
const onConnect = useCallback((params: any) => setEdges((eds) => addEdge(params, eds)), [setEdges])
const onNodeDrag = useCallback(
(_: any, node: any) => {
// Update positions of connected nodes during drag
setNodes((nds) => {
return nds.map((n) => {
if (n.type === 'keyStatus' && n.id === `${node.id}-keys`) {
return {
...n,
position: {
x: 750,
y: node.position.y
}
}
}
return n
})
})
},
[setNodes]
)
const onNodeDragStop = useCallback(
(_: any, node: any) => {
// Reorder nodes based on vertical position
setNodes((nds) => {
const modelNodes = nds.filter((n) => n.type === 'modelCard')
const sortedNodes = [...modelNodes].sort((a, b) => a.position.y - b.position.y)
return nds.map((n) => {
if (n.type === 'modelCard') {
const index = sortedNodes.findIndex((sn) => sn.id === n.id)
return {
...n,
position: {
x: 400,
y: 50 + index * 120
}
}
}
if (n.type === 'keyStatus') {
const modelId = n.id.replace('-keys', '')
const modelNode = sortedNodes.find((mn) => mn.id === modelId)
if (modelNode) {
const index = sortedNodes.findIndex((sn) => sn.id === modelId)
return {
...n,
position: {
x: 750,
y: 50 + index * 120
}
}
}
}
return n
})
})
},
[setNodes]
)
return (
<div className="w-full h-screen bg-gray-50">
<ReactFlow
@@ -221,6 +281,8 @@ const Playground = () => {
onNodesChange={onNodesChange}
onEdgesChange={onEdgesChange}
onConnect={onConnect}
onNodeDrag={onNodeDrag}
onNodeDragStop={onNodeDragStop}
nodeTypes={nodeTypes}
defaultEdgeOptions={{
type: 'step',
@@ -228,7 +290,7 @@ const Playground = () => {
animated: true
}}
fitView
nodesDraggable={false}
nodesDraggable={true}
nodesConnectable={false}
zoomOnScroll={false}
zoomOnPinch={false}