mirror of
https://github.com/APIParkLab/APIPark.git
synced 2026-06-14 20:41:15 +08:00
feat: draggable
This commit is contained in:
@@ -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}
|
||||
|
||||
Reference in New Issue
Block a user