diff --git a/apps/web/src/components/topology/palette.tsx b/apps/web/src/components/topology/palette.tsx index f5d1577..ef06fd2 100644 --- a/apps/web/src/components/topology/palette.tsx +++ b/apps/web/src/components/topology/palette.tsx @@ -7,13 +7,6 @@ import { StickyNoteIcon, ServerIcon, } from 'lucide-react' -import { Button } from '@cfdm/ui/components/button' -import { - Tooltip, - TooltipContent, - TooltipProvider, - TooltipTrigger, -} from '@cfdm/ui/components/tooltip' import { cn } from '@cfdm/ui/lib/utils' import type { PaletteItem, ShapeKind } from './types' @@ -50,14 +43,13 @@ const ITEMS: { item: PaletteItem; icon: typeof SquareIcon; title: string }[] = [ }, ] -const DND_TYPE = 'application/topology-palette' - -export function topologyDnDType(): string { - return DND_TYPE -} +/** MIME used by React Flow drag-and-drop examples */ +export const TOPOLOGY_DND_MIME = 'application/reactflow' export function parsePaletteDrag(dataTransfer: DataTransfer): PaletteItem | null { - const raw = dataTransfer.getData(DND_TYPE) + const raw = + dataTransfer.getData(TOPOLOGY_DND_MIME) || + dataTransfer.getData('text/plain') if (!raw) return null try { return JSON.parse(raw) as PaletteItem @@ -69,63 +61,82 @@ export function parsePaletteDrag(dataTransfer: DataTransfer): PaletteItem | null interface TopologyPaletteProps { className?: string onPickVps: () => void + onPlaceItem?: (item: PaletteItem) => void disabled?: boolean } -export function TopologyPalette({ className, onPickVps, disabled }: TopologyPaletteProps) { +export function TopologyPalette({ + className, + onPickVps, + onPlaceItem, + disabled, +}: TopologyPaletteProps) { + function handlePick(item: PaletteItem) { + if (disabled) return + if (item.kind === 'vps-picker') { + onPickVps() + return + } + onPlaceItem?.(item) + } + return ( - +
- - - } - > - - - Выделение - - {ITEMS.map(({ item, icon: Icon, title }) => ( - - { - if (item.kind === 'vps-picker') return - e.dataTransfer.setData(DND_TYPE, JSON.stringify(item)) - e.dataTransfer.effectAllowed = 'move' - }} - onClick={() => { - if (item.kind === 'vps-picker') onPickVps() - }} - aria-label={title} - /> - } - > - - - {title} - - ))} +
- + {ITEMS.map(({ item, icon: Icon, title }) => { + const canDrag = !disabled && item.kind !== 'vps-picker' + return ( +
{ + if (!canDrag) { + e.preventDefault() + return + } + const payload = JSON.stringify(item) + e.dataTransfer.setData(TOPOLOGY_DND_MIME, payload) + e.dataTransfer.setData('text/plain', payload) + e.dataTransfer.effectAllowed = 'move' + }} + onClick={() => handlePick(item)} + onKeyDown={(e) => { + if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault() + handlePick(item) + } + }} + > + +
+ ) + })} +
) } diff --git a/apps/web/src/components/topology/topology-editor.tsx b/apps/web/src/components/topology/topology-editor.tsx index b015763..b412a5e 100644 --- a/apps/web/src/components/topology/topology-editor.tsx +++ b/apps/web/src/components/topology/topology-editor.tsx @@ -201,6 +201,7 @@ function TopologyEditorInner({ function onDrop(e: DragEvent) { e.preventDefault() + e.stopPropagation() if (locked) return const item = parsePaletteDrag(e.dataTransfer) if (!item) return @@ -222,6 +223,16 @@ function TopologyEditorInner({ setNodes((ns) => [...ns, ...created]) } + function placeItemAtCenter(item: PaletteItem) { + if (locked) return + const rect = wrapperRef.current?.getBoundingClientRect() + const position = screenToFlowPosition({ + x: (rect?.left ?? 0) + (rect?.width ?? 400) / 2, + y: (rect?.top ?? 0) + (rect?.height ?? 300) / 2, + }) + placeNode(item, position) + } + function onNodeClick(_e: ReactMouseEvent, node: FlowNode) { if (node.type === 'vps' && isVpsNodeData(node.data)) { setDetailVpsId(node.data.vpsId) @@ -265,6 +276,8 @@ function TopologyEditorInner({
-
-
- setAddVpsOpen(true)} - /> -
-
- void zoomIn()} - onZoomOut={() => void zoomOut()} - onFitView={() => void fitView({ padding: 0.2 })} - onToggleLock={() => onLockedChange(!locked)} - onFullscreen={() => void handleFullscreen()} - onExport={() => void handleExport()} - /> -
+
+ setAddVpsOpen(true)} + onPlaceItem={placeItemAtCenter} + /> +
+
+ void zoomIn()} + onZoomOut={() => void zoomOut()} + onFitView={() => void fitView({ padding: 0.2 })} + onToggleLock={() => onLockedChange(!locked)} + onFullscreen={() => void handleFullscreen()} + onExport={() => void handleExport()} + />