diff --git a/examples/react/src/App/routes.ts b/examples/react/src/App/routes.ts index 7cd3eb13..1d68c858 100644 --- a/examples/react/src/App/routes.ts +++ b/examples/react/src/App/routes.ts @@ -50,6 +50,7 @@ import UseNodesInitialized from '../examples/UseNodesInit'; import UseNodesData from '../examples/UseNodesData'; import UseHandleConnections from '../examples/UseHandleConnections'; import AddNodeOnEdgeDrop from '../examples/AddNodeOnEdgeDrop'; +import DevTools from '../examples/DevTools'; export interface IRoute { name: string; @@ -113,6 +114,11 @@ const routes: IRoute[] = [ path: 'default-nodes', component: DefaultNodes, }, + { + name: 'DevTools', + path: 'devtools', + component: DevTools, + }, { name: 'Drag Handle', path: 'draghandle', diff --git a/examples/react/src/examples/DevTools/DevTools/ChangeLogger.tsx b/examples/react/src/examples/DevTools/DevTools/ChangeLogger.tsx new file mode 100644 index 00000000..d4fffcda --- /dev/null +++ b/examples/react/src/examples/DevTools/DevTools/ChangeLogger.tsx @@ -0,0 +1,74 @@ +import { useEffect, useRef, useState } from 'react'; +import { NodeChange, OnNodesChange, useStore, useStoreApi } from '@xyflow/react'; + +type ChangeLoggerProps = { + color?: string; + limit?: number; +}; + +type ChangeInfoProps = { + change: NodeChange; +}; + +function ChangeInfo({ change }: ChangeInfoProps) { + const id = 'id' in change ? change.id : '-'; + const { type } = change; + + return ( +
+
node id: {id}
+
+ {type === 'add' ? JSON.stringify(change.item, null, 2) : null} + {type === 'dimensions' ? `${change.dimensions?.width} × ${change.dimensions?.height}` : null} + {type === 'position' ? `position: ${change.position?.x.toFixed(1)}, ${change.position?.y.toFixed(1)}` : null} + {type === 'remove' ? 'remove' : null} + {type === 'replace' ? JSON.stringify(change.item, null, 2) : null} + {type === 'select' ? (change.selected ? 'select' : 'unselect') : null} +
+
+ ); +} + +export default function ChangeLogger({ limit = 20 }: ChangeLoggerProps) { + const [changes, setChanges] = useState([]); + const onNodesChangeIntercepted = useRef(false); + const onNodesChange = useStore((s) => s.onNodesChange); + const store = useStoreApi(); + + useEffect(() => { + if (!onNodesChange || onNodesChangeIntercepted.current) { + return; + } + + onNodesChangeIntercepted.current = true; + const userOnNodesChange = onNodesChange; + + const onNodesChangeLogger: OnNodesChange = (changes) => { + userOnNodesChange(changes); + + setChanges((c) => { + changes.forEach((change) => { + if (c.length >= limit) { + c.pop(); + } + + c = [change, ...c]; + }); + return c; + }); + }; + + store.setState({ onNodesChange: onNodesChangeLogger }); + }, [onNodesChange]); + + return ( +
+
Change Logger
+ {changes.length === 0 ? ( + <>no changes triggered + ) : ( + changes.map((change, index) => ) + )} +
+ ); +} diff --git a/examples/react/src/examples/DevTools/DevTools/NodeInspector.tsx b/examples/react/src/examples/DevTools/DevTools/NodeInspector.tsx new file mode 100644 index 00000000..5d467514 --- /dev/null +++ b/examples/react/src/examples/DevTools/DevTools/NodeInspector.tsx @@ -0,0 +1,68 @@ +import { useNodes, ViewportPortal } from '@xyflow/react'; + +type NodeInfoProps = { + id: string; + type: string; + x: number; + y: number; + width?: number; + height?: number; + data: any; +}; + +function NodeInfo({ id, type, x, y, width, height, data }: NodeInfoProps) { + if (!width || !height) { + return null; + } + + return ( +
+
id: {id}
+
type: {type}
+
+ position: {x.toFixed(1)}, {y.toFixed(1)} +
+
+ dimensions: {width} × {height} +
+
data: {JSON.stringify(data, null, 2)}
+
+ ); +} + +export default function NodeInspector() { + const nodes = useNodes(); + + return ( + +
+ {nodes.map((node) => { + const x = node.computed?.positionAbsolute?.x || 0; + const y = node.computed?.positionAbsolute?.y || 0; + const width = node.computed?.width || 0; + const height = node.computed?.height || 0; + + return ( + + ); + })} +
+
+ ); +} diff --git a/examples/react/src/examples/DevTools/DevTools/index.tsx b/examples/react/src/examples/DevTools/DevTools/index.tsx new file mode 100644 index 00000000..107d0078 --- /dev/null +++ b/examples/react/src/examples/DevTools/DevTools/index.tsx @@ -0,0 +1,42 @@ +import { useState, type Dispatch, type SetStateAction, type ReactNode, HTMLAttributes } from 'react'; +import { Panel } from '@xyflow/react'; + +import NodeInspector from './NodeInspector'; +import ChangeLogger from './ChangeLogger'; + +export default function ReactFlowDevTools() { + const [nodeInspectorActive, setNodeInspectorActive] = useState(false); + const [changeLoggerActive, setChangeLoggerActive] = useState(false); + + return ( +
+ + + Node Inspector + + + Change Logger + + + {changeLoggerActive && } + {nodeInspectorActive && } +
+ ); +} + +function DevToolButton({ + active, + setActive, + children, + ...rest +}: { + active: boolean; + setActive: Dispatch>; + children: ReactNode; +} & HTMLAttributes) { + return ( + + ); +} diff --git a/examples/react/src/examples/DevTools/DevTools/style.css b/examples/react/src/examples/DevTools/DevTools/style.css new file mode 100644 index 00000000..39c65969 --- /dev/null +++ b/examples/react/src/examples/DevTools/DevTools/style.css @@ -0,0 +1,63 @@ +.react-flow__devtools { + --border-radius: 4px; + --highlight-color: rgba(238, 58, 115, 1); + --font: monospace, sans-serif; + + border-radius: var(--border-radius); + font-size: 11px; + font-family: var(--font); +} + +.react-flow__devtools button { + background: white; + border: none; + padding: 5px 15px; + color: #222; + font-weight: bold; + font-size: 12px; + cursor: pointer; + font-family: var(--font); + background-color: #f4f4f4; +} + +.react-flow__devtools button:hover { + background: var(--highlight-color); + color: white; +} + +.react-flow__devtools button.active { + background: var(--highlight-color); + color: white; +} + +.react-flow__devtools button:first-child { + border-radius: var(--border-radius) 0 0 var(--border-radius); + border-right: 1px solid #ddd; +} + +.react-flow__devtools button:last-child { + border-radius: 0 var(--border-radius) var(--border-radius) 0; +} + +.react-flow__devtools-changelogger { + pointer-events: none; + position: relative; + top: 50px; + left: 20px; + font-family: var(--font); +} + +.react-flow__devtools-title { + font-weight: bold; + margin-bottom: 5px; +} + +.react-flow__devtools-nodeinspector { + pointer-events: none; + font-family: monospace, sans-serif; + font-size: 10px; +} + +.react-flow__devtools-nodeinfo { + top: 5px; +} diff --git a/examples/react/src/examples/DevTools/index.tsx b/examples/react/src/examples/DevTools/index.tsx new file mode 100644 index 00000000..5303dea6 --- /dev/null +++ b/examples/react/src/examples/DevTools/index.tsx @@ -0,0 +1,56 @@ +import { useCallback } from 'react'; +import { ReactFlow, addEdge, Node, Connection, Edge, useNodesState, useEdgesState } from '@xyflow/react'; + +import DevTools from './DevTools'; +import './Devtools/style.css'; + +const initNodes: Node[] = [ + { + id: '1a', + type: 'input', + data: { label: 'Node 1' }, + position: { x: 250, y: 5 }, + }, + { + id: '2a', + data: { label: 'Node 2' }, + position: { x: 100, y: 100 }, + }, + { + id: '3a', + data: { label: 'Node 3' }, + position: { x: 400, y: 100 }, + }, + { + id: '4a', + data: { label: 'Node 4' }, + position: { x: 400, y: 200 }, + }, +]; + +const initEdges: Edge[] = [ + { id: 'e1-2', source: '1a', target: '2a' }, + { id: 'e1-3', source: '1a', target: '3a' }, +]; + +const BasicFlow = () => { + const [nodes, setNodes, onNodesChange] = useNodesState(initNodes); + const [edges, setEdges, onEdgesChange] = useEdgesState(initEdges); + + const onConnect = useCallback((params: Connection | Edge) => setEdges((eds) => addEdge(params, eds)), [setEdges]); + + return ( + + + + ); +}; + +export default BasicFlow;