feat(devtools): add change logger
This commit is contained in:
@@ -0,0 +1,82 @@
|
|||||||
|
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, index }: ChangeInfoProps) {
|
||||||
|
const id = 'id' in change ? change.id : '-';
|
||||||
|
const { type } = change;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={{ marginBottom: 4 }}>
|
||||||
|
<div>node id: {id}</div>
|
||||||
|
<div>
|
||||||
|
{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}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function ChangeLogger({ color = '#555', limit = 20 }: ChangeLoggerProps) {
|
||||||
|
const [changes, setChanges] = useState<NodeChange[]>([]);
|
||||||
|
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);
|
||||||
|
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
setChanges((c) => {
|
||||||
|
changes.forEach((change) => {
|
||||||
|
if (c.length >= limit) {
|
||||||
|
c.pop();
|
||||||
|
}
|
||||||
|
|
||||||
|
c = [change, ...c];
|
||||||
|
});
|
||||||
|
return c;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
store.setState({ onNodesChange: onNodesChangeLogger });
|
||||||
|
}, [onNodesChange]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
style={{ fontFamily: 'monospace', fontSize: 10, position: 'relative', top: 45, left: 15, width: '100%', color }}
|
||||||
|
>
|
||||||
|
<div style={{ margin: '5px 0' }}>
|
||||||
|
<strong>Change Logger</strong>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
{changes.length === 0 ? (
|
||||||
|
<>no changes triggered</>
|
||||||
|
) : (
|
||||||
|
changes.map((change, index) => <ChangeInfo key={index} change={change} />)
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,40 +1,74 @@
|
|||||||
import { useNodes, ViewportPortal } from '@xyflow/react';
|
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 <div></div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
position: 'absolute',
|
||||||
|
zIndex: 9999,
|
||||||
|
transform: `translate(${x}px, ${y + height}px)`,
|
||||||
|
padding: 5,
|
||||||
|
boxSizing: 'border-box',
|
||||||
|
width: width * 2,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div>id: {id}</div>
|
||||||
|
<div>type: {type}</div>
|
||||||
|
<div>
|
||||||
|
position: {x.toFixed(1)}, {y.toFixed(1)}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
dimensions: {width} × {height}
|
||||||
|
</div>
|
||||||
|
<div>data: {JSON.stringify(data, null, 2)}</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
type NodeInspectorProps = {
|
type NodeInspectorProps = {
|
||||||
color: string;
|
color: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function NodeInspector({ color = '#aaa' }: NodeInspectorProps) {
|
export default function NodeInspector({ color = '#555' }: NodeInspectorProps) {
|
||||||
const nodes = useNodes();
|
const nodes = useNodes();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ViewportPortal>
|
<ViewportPortal>
|
||||||
{nodes.map((node) => {
|
<div style={{ color, fontSize: 10, fontFamily: 'monospace' }}>
|
||||||
const x = node.computed?.positionAbsolute?.x || 0;
|
{nodes.map((node) => {
|
||||||
const y = node.computed?.positionAbsolute?.y || 0;
|
const x = node.computed?.positionAbsolute?.x || 0;
|
||||||
const height = node.computed?.height || 0;
|
const y = node.computed?.positionAbsolute?.y || 0;
|
||||||
|
const width = node.computed?.width || 0;
|
||||||
|
const height = node.computed?.height || 0;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<NodeInfo
|
||||||
key={node.id}
|
key={node.id}
|
||||||
style={{
|
id={node.id}
|
||||||
position: 'absolute',
|
type={node.type || 'default'}
|
||||||
fontSize: 10,
|
x={x}
|
||||||
color: color,
|
y={y}
|
||||||
pointerEvents: 'none',
|
width={width}
|
||||||
zIndex: 9999,
|
height={height}
|
||||||
transform: `translate(${x}px, ${y + height}px)`,
|
data={node.data}
|
||||||
}}
|
/>
|
||||||
>
|
);
|
||||||
<div>id: {node.id}</div>
|
})}
|
||||||
<div>type: {node.type}</div>
|
</div>
|
||||||
<div>
|
|
||||||
position: {x.toFixed(1)}, {y.toFixed(1)}
|
|
||||||
</div>
|
|
||||||
<div>data: {JSON.stringify(node.data, null, 2)}</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</ViewportPortal>
|
</ViewportPortal>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { useState } from 'react';
|
|||||||
import { Panel, PanelPosition } from '@xyflow/react';
|
import { Panel, PanelPosition } from '@xyflow/react';
|
||||||
|
|
||||||
import NodeInspector from './NodeInspector';
|
import NodeInspector from './NodeInspector';
|
||||||
|
import ChangeLogger from './ChangeLogger';
|
||||||
|
|
||||||
type ReactFlowDevToolsProps = {
|
type ReactFlowDevToolsProps = {
|
||||||
position?: PanelPosition;
|
position?: PanelPosition;
|
||||||
@@ -10,13 +11,16 @@ type ReactFlowDevToolsProps = {
|
|||||||
|
|
||||||
export default function ReactFlowDevTools({ color = '#aaa', position = 'top-left' }: ReactFlowDevToolsProps) {
|
export default function ReactFlowDevTools({ color = '#aaa', position = 'top-left' }: ReactFlowDevToolsProps) {
|
||||||
const [isNodeInspectorActive, setIsNodeInspectorActive] = useState(false);
|
const [isNodeInspectorActive, setIsNodeInspectorActive] = useState(false);
|
||||||
|
const [isChangeLoggerActive, setIsChangeLoggerActive] = useState(false);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Panel position={position}>
|
<Panel position={position}>
|
||||||
<button onClick={() => setIsNodeInspectorActive((a) => !a)}>node inspector</button>
|
<button onClick={() => setIsNodeInspectorActive((a) => !a)}>node inspector</button>
|
||||||
|
<button onClick={() => setIsChangeLoggerActive((a) => !a)}>change logger</button>
|
||||||
</Panel>
|
</Panel>
|
||||||
{isNodeInspectorActive && <NodeInspector color={color} />}
|
{isNodeInspectorActive && <NodeInspector color={color} />}
|
||||||
|
{isChangeLoggerActive && <ChangeLogger color={color} />}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user