From b5e7fa3a7000269859c8752e677a00a30da517ac Mon Sep 17 00:00:00 2001 From: ghun131 Date: Sun, 12 Nov 2023 16:31:21 +0700 Subject: [PATCH 1/5] feat: auto layout and remove CollabPanel --- .../webview-ui/src/components/CollabPanel.tsx | 42 ----- dnd-ui/webview-ui/src/hooks/useAutoLayout.tsx | 164 ++++++++++++++++++ dnd-ui/webview-ui/src/pages/Flow.tsx | 22 +-- 3 files changed, 171 insertions(+), 57 deletions(-) delete mode 100644 dnd-ui/webview-ui/src/components/CollabPanel.tsx create mode 100644 dnd-ui/webview-ui/src/hooks/useAutoLayout.tsx diff --git a/dnd-ui/webview-ui/src/components/CollabPanel.tsx b/dnd-ui/webview-ui/src/components/CollabPanel.tsx deleted file mode 100644 index 10f45fa..0000000 --- a/dnd-ui/webview-ui/src/components/CollabPanel.tsx +++ /dev/null @@ -1,42 +0,0 @@ -// @ts-nocheck -import { useState } from 'react'; -import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { faHandshake } from '@fortawesome/free-regular-svg-icons'; -import { memo } from 'react'; -import { Panel } from 'reactflow'; -import { getWebSocket } from '../socket/getWebSocket'; -console.log('getWebSocket:', getWebSocket); - -function SavePanel() { - const [isCollab, setCollab] = useState(true); - const style = 'rounded !text-white font-semibold py-2 px-5 cursor-pointer'; - - function handleCollab(newStt) { - setCollab(newStt); - const ws = getWebSocket(); - if (newStt) { - ws.connect(); - } else { - ws.destroy(); - } - } - - return ( - handleCollab(!isCollab)} - > - - {isCollab ? ( - - ) : ( - - )} - - - ); -} - -export default memo(SavePanel); diff --git a/dnd-ui/webview-ui/src/hooks/useAutoLayout.tsx b/dnd-ui/webview-ui/src/hooks/useAutoLayout.tsx new file mode 100644 index 0000000..504df06 --- /dev/null +++ b/dnd-ui/webview-ui/src/hooks/useAutoLayout.tsx @@ -0,0 +1,164 @@ +import { useEffect } from 'react'; +import { + Node, + Edge, + Position, + ReactFlowState, + useStore, + useReactFlow, +} from 'reactflow'; +import { stratify, tree } from 'd3-hierarchy'; + +// the layout direction (T = top, R = right, B = bottom, L = left, TB = top to bottom, ...) +export type Direction = 'TB' | 'LR' | 'RL' | 'BT'; + +export type Options = { + direction: Direction; +}; + +const positionMap: Record = { + T: Position.Top, + L: Position.Left, + R: Position.Right, + B: Position.Bottom, +}; + +const getPosition = (x: number, y: number, direction: Direction) => { + switch (direction) { + case 'LR': + return { x: y, y: x }; + case 'RL': + return { x: -y, y: -x }; + case 'BT': + return { x: -x, y: -y }; + default: + return { x, y }; + } +}; + +// initialize the tree layout (see https://observablehq.com/@d3/tree for examples) +const layout = tree() + // the node size configures the spacing between the nodes ([width, height]) + .nodeSize([130, 120]) + // this is needed for creating equal space between all nodes + .separation(() => 1); + +const nodeCountSelector = (state: ReactFlowState) => state.nodeInternals.size; +const nodesInitializedSelector = (state: ReactFlowState) => + Array.from(state.nodeInternals.values()).every( + (node) => node.width && node.height + ); + +function useAutoLayout(options: Options) { + const { direction } = options; + const nodeCount = useStore(nodeCountSelector); + const nodesInitialized = useStore(nodesInitializedSelector); + const { getNodes, getEdges, setNodes, setEdges, fitView } = useReactFlow(); + + // useEffect(() => { + // // only run the layout if there are nodes and they have been initialized with their dimensions + // if (!nodeCount || !nodesInitialized) { + // return; + // } + + // const nodes: Node[] = getNodes(); + // const edges: Edge[] = getEdges(); + // console.log('edges:', edges); + // console.log('nodes:', nodes); + + // const hierarchy = stratify() + // .id((d) => d.id) + // // get the id of each node by searching through the edges + // // this only works if every node has one connection + // .parentId( + // (d: Node) => edges.find((e: Edge) => e.target === d.id)?.source + // )(nodes); + // console.log('hierarchy:', hierarchy); + + // // run the layout algorithm with the hierarchy data structure + // const root = layout(hierarchy); + + // // set the React Flow nodes with the positions from the layout + // setNodes((nodes) => + // nodes.map((node) => { + // // find the node in the hierarchy with the same id and get its coordinates + // const { x, y } = root.find((d) => d.id === node.id) || { + // x: node.position.x, + // y: node.position.y, + // }; + + // return { + // ...node, + // sourcePosition: positionMap[direction[1]], + // targetPosition: positionMap[direction[0]], + // position: getPosition(x, y, direction), + // style: { opacity: 1 }, + // }; + // }) + // ); + + // setEdges((edges) => + // edges.map((edge) => ({ ...edge, style: { opacity: 1 } })) + // ); + // }, [ + // nodeCount, + // nodesInitialized, + // getNodes, + // getEdges, + // setNodes, + // setEdges, + // fitView, + // direction, + // ]); + + function autoLayout() { + // only run the layout if there are nodes and they have been initialized with their dimensions + if (!nodeCount || !nodesInitialized) { + return; + } + + const nodes: Node[] = getNodes(); + const edges: Edge[] = getEdges(); + console.log('edges:', edges); + console.log('nodes:', nodes); + + const hierarchy = stratify() + .id((d) => d.id) + // get the id of each node by searching through the edges + // this only works if every node has one connection + .parentId( + (d: Node) => edges.find((e: Edge) => e.target === d.id)?.source + )(nodes); + console.log('hierarchy:', hierarchy); + + // run the layout algorithm with the hierarchy data structure + const root = layout(hierarchy); + + // set the React Flow nodes with the positions from the layout + setNodes((nodes) => + nodes.map((node) => { + // find the node in the hierarchy with the same id and get its coordinates + const { x, y } = root.find((d) => d.id === node.id) || { + x: node.position.x, + y: node.position.y, + }; + + return { + ...node, + sourcePosition: positionMap[direction[1]], + targetPosition: positionMap[direction[0]], + position: getPosition(x, y, direction), + style: { opacity: 1 }, + }; + }) + ); + + setEdges((edges) => + edges.map((edge) => ({ ...edge, style: { opacity: 1 } })) + ); + } + + return { autoLayout }; +} + +export default useAutoLayout; diff --git a/dnd-ui/webview-ui/src/pages/Flow.tsx b/dnd-ui/webview-ui/src/pages/Flow.tsx index d9a8d0a..81e70b6 100644 --- a/dnd-ui/webview-ui/src/pages/Flow.tsx +++ b/dnd-ui/webview-ui/src/pages/Flow.tsx @@ -22,12 +22,12 @@ import ReactFlow, { updateEdge, useEdgesState, useNodesState, + useReactFlow, } from 'reactflow'; import YAML from 'yaml'; import { toast } from 'react-toastify'; import { Compiler } from '../compilers'; -import CollabPanel from '../components/CollabPanel'; import CompilePanel from '../components/CompilePanel'; import CustomEdge from '../components/CustomEdge'; import MenuPanel from '../components/MenuPanel'; @@ -87,6 +87,7 @@ const edgeTypes = { const Editor = () => { const { height: windowHeight, width: windowWidth } = useGetWindowSize(); const { takeSnapshot, undo, redo, setPast } = useUndoRedo(); + // useAutoLayout({ direction: 'TB' }); const [nodes, setNodes] = useNodesState(initialNodes); const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges); @@ -104,12 +105,17 @@ const Editor = () => { const canPaste = bufferedNodes.length > 0; const viewport = useRef(defaultViewport); + const { fitView } = useReactFlow(); useEffect(() => { window.addEventListener('message', handleCallback); () => window.removeEventListener('message', handleCallback); }, []); + useEffect(() => { + fitView({ duration: 400 }); + }, [nodes, fitView]); + const onConnect = useCallback( (connection: Connection) => { takeSnapshot(); @@ -238,19 +244,6 @@ const Editor = () => { setStore({ ...payload }); const yamlData = YAML.stringify(payload); - // When testing on browser, the vscode.postMessage won't work - // we will manually emit fileUpdate event - // if (window) { - // const simulateFileUpdateTriggerOnBrowser = new CustomEvent("message", { - // detail: { - // "type": 'fileUpdate', - // "text": yamlData, - // }, - // }); - - // // window.dispatchEvent(simulateFileUpdateTriggerOnBrowser) - // } - vscode.postMessage({ type: 'addEdit', data: { @@ -344,7 +337,6 @@ const Editor = () => { - Date: Tue, 28 Nov 2023 08:19:11 +0700 Subject: [PATCH 2/5] feat: dynamic layouts --- .../src/components/NodeMenuPanel.tsx | 8 + .../webview-ui/src/hooks/useDetachNodes.tsx | 36 ++ dnd-ui/webview-ui/src/nodes/GroupNode.tsx | 86 +++++ dnd-ui/webview-ui/src/pages/Flow.css | 2 +- dnd-ui/webview-ui/src/pages/Flow.tsx | 182 ++++++++- dnd-ui/webview-ui/src/utilities/groupNodes.ts | 40 ++ package.json | 5 + pnpm-lock.yaml | 365 ++++++++++++++++++ 8 files changed, 713 insertions(+), 11 deletions(-) create mode 100644 dnd-ui/webview-ui/src/hooks/useDetachNodes.tsx create mode 100644 dnd-ui/webview-ui/src/nodes/GroupNode.tsx create mode 100644 dnd-ui/webview-ui/src/utilities/groupNodes.ts create mode 100644 package.json create mode 100644 pnpm-lock.yaml diff --git a/dnd-ui/webview-ui/src/components/NodeMenuPanel.tsx b/dnd-ui/webview-ui/src/components/NodeMenuPanel.tsx index 38fa450..a54609f 100644 --- a/dnd-ui/webview-ui/src/components/NodeMenuPanel.tsx +++ b/dnd-ui/webview-ui/src/components/NodeMenuPanel.tsx @@ -91,6 +91,14 @@ function NodeMenuPanel({ CTFlow Recorder +
+ +
{/*
+ {hasChildNodes && ( + + )} + +
+ ); +} + +type IsEqualCompareObj = { + minWidth: number; + minHeight: number; + hasChildNodes: boolean; +}; + +function isEqual(prev: IsEqualCompareObj, next: IsEqualCompareObj): boolean { + return ( + prev.minWidth === next.minWidth && + prev.minHeight === next.minHeight && + prev.hasChildNodes === next.hasChildNodes + ); +} + +export default memo(GroupNode); diff --git a/dnd-ui/webview-ui/src/pages/Flow.css b/dnd-ui/webview-ui/src/pages/Flow.css index a359482..5ca8aa8 100644 --- a/dnd-ui/webview-ui/src/pages/Flow.css +++ b/dnd-ui/webview-ui/src/pages/Flow.css @@ -1,4 +1,4 @@ .selected { - box-shadow: 0 0 2px 3px #ff0071; + box-shadow: 0 0 2px 3px #ff0071 !important; border-radius: 0.5rem; } diff --git a/dnd-ui/webview-ui/src/pages/Flow.tsx b/dnd-ui/webview-ui/src/pages/Flow.tsx index 81e70b6..08fb776 100644 --- a/dnd-ui/webview-ui/src/pages/Flow.tsx +++ b/dnd-ui/webview-ui/src/pages/Flow.tsx @@ -4,7 +4,13 @@ import './flow.css'; import get from 'lodash/get'; import omit from 'lodash/omit'; import pick from 'lodash/pick'; -import { useCallback, useEffect, useRef, useState } from 'react'; +import { + DragEventHandler, + useCallback, + useEffect, + useRef, + useState, +} from 'react'; import ReactFlow, { Background, Connection, @@ -14,6 +20,7 @@ import ReactFlow, { MiniMap, Node, NodeChange, + NodeDragHandler, OnNodesChange, OnSelectionChangeParams, Viewport, @@ -23,6 +30,7 @@ import ReactFlow, { useEdgesState, useNodesState, useReactFlow, + useStoreApi, } from 'reactflow'; import YAML from 'yaml'; @@ -30,15 +38,18 @@ import { toast } from 'react-toastify'; import { Compiler } from '../compilers'; import CompilePanel from '../components/CompilePanel'; import CustomEdge from '../components/CustomEdge'; +import HelperLines from '../components/HelperLine'; import MenuPanel from '../components/MenuPanel'; import NodeMenuPanel from '../components/NodeMenuPanel'; import SavePanel from '../components/SavePanel'; import { useStore } from '../context/store'; +import useCopyPaste from '../hooks/useCopyPaste'; import { useGetWindowSize } from '../hooks/useGetWindowSize'; import useUndoRedo from '../hooks/useUndoRedo'; import AnyNode from '../nodes/AnyNode'; import CTFlowRecorderNode from '../nodes/CTFlowRecorderNode'; import CustomNodeRender from '../nodes/CustomNodeRender'; +import GroupNode from '../nodes/GroupNode'; import ButtonNode from '../nodes/old_nodes/ButtonNode'; import CheckboxNode from '../nodes/old_nodes/CheckboxNode'; import ContainsNode from '../nodes/old_nodes/ContainsNode'; @@ -47,8 +58,11 @@ import VisitPageNode from '../nodes/old_nodes/VisitPageNode'; import WaitNode from '../nodes/old_nodes/WaitNode'; import { getHelperLines } from '../utilities/helperLines'; import { vscode } from '../utilities/vscode'; -import HelperLines from '../components/HelperLine'; -import useCopyPaste from '../hooks/useCopyPaste'; +import { + getId, + getNodePositionInsideParent, + sortNodes, +} from '../utilities/groupNodes'; const fitViewOptions: FitViewOptions = { padding: 0.2, @@ -70,6 +84,7 @@ const nodeTypes = { CTFlowRecorderNode: CTFlowRecorderNode, customNode: CustomNodeRender, anyNode: AnyNode, + group: GroupNode, // TODO: remove old nodes in new version buttonNode: ButtonNode, @@ -87,6 +102,7 @@ const edgeTypes = { const Editor = () => { const { height: windowHeight, width: windowWidth } = useGetWindowSize(); const { takeSnapshot, undo, redo, setPast } = useUndoRedo(); + const wrapperRef = useRef(null); // useAutoLayout({ direction: 'TB' }); const [nodes, setNodes] = useNodesState(initialNodes); @@ -105,7 +121,8 @@ const Editor = () => { const canPaste = bufferedNodes.length > 0; const viewport = useRef(defaultViewport); - const { fitView } = useReactFlow(); + const { project, getIntersectingNodes, fitView } = useReactFlow(); + const rfStore = useStoreApi(); useEffect(() => { window.addEventListener('message', handleCallback); @@ -272,10 +289,6 @@ const Editor = () => { viewport.current = curViewport; } - const dragStop = useCallback(() => { - takeSnapshot(); - }, [takeSnapshot]); - const customApplyNodeChanges = useCallback( (changes: NodeChange[], nodes: Node[]): Node[] => { // reset the helper lines (clear existing lines, if any) @@ -316,17 +329,166 @@ const Editor = () => { [setNodes, customApplyNodeChanges] ); + const onNodeDrag: NodeDragHandler = useCallback( + (_: React.MouseEvent, node: Node) => { + if (node.type !== 'anyNode' && !node.parentNode) { + return; + } + + const intersections = getIntersectingNodes(node).filter( + (n) => n.type === 'group' + ); + const groupClassName = + intersections.length && node.parentNode !== intersections[0]?.id + ? 'active' + : ''; + + setNodes((nds) => { + return nds.map((n) => { + if (n.type === 'group') { + return { + ...n, + className: groupClassName, + }; + } else if (n.id === node.id) { + return { + ...n, + position: node.position, + }; + } + + return { ...n }; + }); + }); + }, + [getIntersectingNodes, setNodes] + ); + const onNodeDragStop = useCallback( + (_: React.MouseEvent, node: Node) => { + if (node.type !== 'anyNode' && !node.parentNode) { + return; + } + + const intersections = getIntersectingNodes(node).filter( + (n) => n.type === 'group' + ); + const groupNode = intersections[0]; + + // when there is an intersection on drag stop, we want to attach the node to its new parent + if (intersections.length && node.parentNode !== groupNode?.id) { + const nextNodes: Node[] = rfStore + .getState() + .getNodes() + .map((n) => { + if (n.id === groupNode.id) { + return { + ...n, + className: '', + }; + } else if (n.id === node.id) { + const position = getNodePositionInsideParent(n, groupNode) ?? { + x: 0, + y: 0, + }; + + return { + ...n, + position, + parentNode: groupNode.id, + // we need to set dragging = false, because the internal change of the dragging state + // is not applied yet, so the node would be rendered as dragging + dragging: false, + extent: 'parent' as 'parent', + }; + } + + return n; + }) + .sort(sortNodes); + + setNodes(nextNodes); + } + takeSnapshot(); + }, + [getIntersectingNodes, setNodes, store, takeSnapshot] + ); + const onDrop: DragEventHandler = (event: React.DragEvent) => { + console.log('onDrop'); + + event.preventDefault(); + console.log('wrapperRef.current:', wrapperRef.current); + + if (wrapperRef.current) { + const wrapperBounds = wrapperRef.current.getBoundingClientRect(); + const type = event.dataTransfer.getData('application/reactflow'); + const position = project({ + x: event.clientX - wrapperBounds.x - 20, + y: event.clientY - wrapperBounds.top - 20, + }); + console.log('position:', position); + const nodeStyle = + type === 'group' ? { width: 400, height: 200 } : undefined; + + const intersections = getIntersectingNodes({ + x: position.x, + y: position.y, + width: 40, + height: 40, + }).filter((n) => n.type === 'group'); + const groupNode = intersections[0]; + + const newNode: Node = { + id: getId(), + type, + position, + data: { label: `${type}` }, + style: nodeStyle, + }; + + if (groupNode) { + // if we drop a node on a group node, we want to position the node inside the group + newNode.position = getNodePositionInsideParent( + { + position, + width: 40, + height: 40, + }, + groupNode + ) ?? { x: 0, y: 0 }; + newNode.parentNode = groupNode?.id; + newNode.extent = groupNode ? 'parent' : undefined; + } + + // we need to make sure that the parents are sorted before the children + // to make sure that the children are rendered on top of the parents + const sortedNodes = rfStore + .getState() + .getNodes() + .concat(newNode) + .sort(sortNodes); + setNodes(sortedNodes); + } + }; + const onDragOver = (event: React.DragEvent) => { + event.preventDefault(); + event.dataTransfer.dropEffect = 'move'; + }; return ( -
+
{ + if (a.type === b.type) { + return 0; + } + return a.type === 'group' && b.type !== 'group' ? -1 : 1; +}; + +export const getId = (prefix = 'node') => `${prefix}_${Math.random() * 10000}`; + +export const getNodePositionInsideParent = ( + node: Partial, + groupNode: Node +) => { + const position = node.position ?? { x: 0, y: 0 }; + const nodeWidth = node.width ?? 0; + const nodeHeight = node.height ?? 0; + const groupWidth = groupNode.width ?? 0; + const groupHeight = groupNode.height ?? 0; + + if (position.x < groupNode.position.x) { + position.x = 0; + } else if (position.x + nodeWidth > groupNode.position.x + groupWidth) { + position.x = groupWidth - nodeWidth; + } else { + position.x = position.x - groupNode.position.x; + } + + if (position.y < groupNode.position.y) { + position.y = 0; + } else if (position.y + nodeHeight > groupNode.position.y + groupHeight) { + position.y = groupHeight - nodeHeight; + } else { + position.y = position.y - groupNode.position.y; + } + + return position; +}; diff --git a/package.json b/package.json new file mode 100644 index 0000000..04fe730 --- /dev/null +++ b/package.json @@ -0,0 +1,5 @@ +{ + "dependencies": { + "@reactflow/node-resizer": "^2.2.6" + } +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml new file mode 100644 index 0000000..708176f --- /dev/null +++ b/pnpm-lock.yaml @@ -0,0 +1,365 @@ +lockfileVersion: '6.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +dependencies: + '@reactflow/node-resizer': + specifier: ^2.2.6 + version: 2.2.6(react-dom@18.2.0)(react@18.2.0) + +packages: + + /@reactflow/core@11.10.1(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-GIh3usY1W3eVobx//OO9+Cwm+5evQBBdPGxDaeXwm25UqPMWRI240nXQA5F/5gL5Mwpf0DUC7DR2EmrKNQy+Rw==} + peerDependencies: + react: '>=17' + react-dom: '>=17' + dependencies: + '@types/d3': 7.4.3 + '@types/d3-drag': 3.0.7 + '@types/d3-selection': 3.0.10 + '@types/d3-zoom': 3.0.8 + classcat: 5.0.4 + d3-drag: 3.0.0 + d3-selection: 3.0.0 + d3-zoom: 3.0.0 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + zustand: 4.4.6(react@18.2.0) + transitivePeerDependencies: + - '@types/react' + - immer + dev: false + + /@reactflow/node-resizer@2.2.6(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-1Xb6q97uP7hRBLpog9sRCNfnsHdDgFRGEiU+lQqGgPEAeYwl4nRjWa/sXwH6ajniKxBhGEvrdzOgEFn6CRMcpQ==} + peerDependencies: + react: '>=17' + react-dom: '>=17' + dependencies: + '@reactflow/core': 11.10.1(react-dom@18.2.0)(react@18.2.0) + classcat: 5.0.4 + d3-drag: 3.0.0 + d3-selection: 3.0.0 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + zustand: 4.4.6(react@18.2.0) + transitivePeerDependencies: + - '@types/react' + - immer + dev: false + + /@types/d3-array@3.2.1: + resolution: {integrity: sha512-Y2Jn2idRrLzUfAKV2LyRImR+y4oa2AntrgID95SHJxuMUrkNXmanDSed71sRNZysveJVt1hLLemQZIady0FpEg==} + dev: false + + /@types/d3-axis@3.0.6: + resolution: {integrity: sha512-pYeijfZuBd87T0hGn0FO1vQ/cgLk6E1ALJjfkC0oJ8cbwkZl3TpgS8bVBLZN+2jjGgg38epgxb2zmoGtSfvgMw==} + dependencies: + '@types/d3-selection': 3.0.10 + dev: false + + /@types/d3-brush@3.0.6: + resolution: {integrity: sha512-nH60IZNNxEcrh6L1ZSMNA28rj27ut/2ZmI3r96Zd+1jrZD++zD3LsMIjWlvg4AYrHn/Pqz4CF3veCxGjtbqt7A==} + dependencies: + '@types/d3-selection': 3.0.10 + dev: false + + /@types/d3-chord@3.0.6: + resolution: {integrity: sha512-LFYWWd8nwfwEmTZG9PfQxd17HbNPksHBiJHaKuY1XeqscXacsS2tyoo6OdRsjf+NQYeB6XrNL3a25E3gH69lcg==} + dev: false + + /@types/d3-color@3.1.3: + resolution: {integrity: sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==} + dev: false + + /@types/d3-contour@3.0.6: + resolution: {integrity: sha512-BjzLgXGnCWjUSYGfH1cpdo41/hgdWETu4YxpezoztawmqsvCeep+8QGfiY6YbDvfgHz/DkjeIkkZVJavB4a3rg==} + dependencies: + '@types/d3-array': 3.2.1 + '@types/geojson': 7946.0.13 + dev: false + + /@types/d3-delaunay@6.0.4: + resolution: {integrity: sha512-ZMaSKu4THYCU6sV64Lhg6qjf1orxBthaC161plr5KuPHo3CNm8DTHiLw/5Eq2b6TsNP0W0iJrUOFscY6Q450Hw==} + dev: false + + /@types/d3-dispatch@3.0.6: + resolution: {integrity: sha512-4fvZhzMeeuBJYZXRXrRIQnvUYfyXwYmLsdiN7XXmVNQKKw1cM8a5WdID0g1hVFZDqT9ZqZEY5pD44p24VS7iZQ==} + dev: false + + /@types/d3-drag@3.0.7: + resolution: {integrity: sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ==} + dependencies: + '@types/d3-selection': 3.0.10 + dev: false + + /@types/d3-dsv@3.0.7: + resolution: {integrity: sha512-n6QBF9/+XASqcKK6waudgL0pf/S5XHPPI8APyMLLUHd8NqouBGLsU8MgtO7NINGtPBtk9Kko/W4ea0oAspwh9g==} + dev: false + + /@types/d3-ease@3.0.2: + resolution: {integrity: sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==} + dev: false + + /@types/d3-fetch@3.0.7: + resolution: {integrity: sha512-fTAfNmxSb9SOWNB9IoG5c8Hg6R+AzUHDRlsXsDZsNp6sxAEOP0tkP3gKkNSO/qmHPoBFTxNrjDprVHDQDvo5aA==} + dependencies: + '@types/d3-dsv': 3.0.7 + dev: false + + /@types/d3-force@3.0.9: + resolution: {integrity: sha512-IKtvyFdb4Q0LWna6ymywQsEYjK/94SGhPrMfEr1TIc5OBeziTi+1jcCvttts8e0UWZIxpasjnQk9MNk/3iS+kA==} + dev: false + + /@types/d3-format@3.0.4: + resolution: {integrity: sha512-fALi2aI6shfg7vM5KiR1wNJnZ7r6UuggVqtDA+xiEdPZQwy/trcQaHnwShLuLdta2rTymCNpxYTiMZX/e09F4g==} + dev: false + + /@types/d3-geo@3.1.0: + resolution: {integrity: sha512-856sckF0oP/diXtS4jNsiQw/UuK5fQG8l/a9VVLeSouf1/PPbBE1i1W852zVwKwYCBkFJJB7nCFTbk6UMEXBOQ==} + dependencies: + '@types/geojson': 7946.0.13 + dev: false + + /@types/d3-hierarchy@3.1.6: + resolution: {integrity: sha512-qlmD/8aMk5xGorUvTUWHCiumvgaUXYldYjNVOWtYoTYY/L+WwIEAmJxUmTgr9LoGNG0PPAOmqMDJVDPc7DOpPw==} + dev: false + + /@types/d3-interpolate@3.0.4: + resolution: {integrity: sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==} + dependencies: + '@types/d3-color': 3.1.3 + dev: false + + /@types/d3-path@3.0.2: + resolution: {integrity: sha512-WAIEVlOCdd/NKRYTsqCpOMHQHemKBEINf8YXMYOtXH0GA7SY0dqMB78P3Uhgfy+4X+/Mlw2wDtlETkN6kQUCMA==} + dev: false + + /@types/d3-polygon@3.0.2: + resolution: {integrity: sha512-ZuWOtMaHCkN9xoeEMr1ubW2nGWsp4nIql+OPQRstu4ypeZ+zk3YKqQT0CXVe/PYqrKpZAi+J9mTs05TKwjXSRA==} + dev: false + + /@types/d3-quadtree@3.0.5: + resolution: {integrity: sha512-Cb1f3jyNBnvMMkf4KBZ7IgAQVWd9yzBwYcrxGqg3aPCUgWELAS+nyeB7r76aqu1e3+CGDjhk4BrWaFBekMwigg==} + dev: false + + /@types/d3-random@3.0.3: + resolution: {integrity: sha512-Imagg1vJ3y76Y2ea0871wpabqp613+8/r0mCLEBfdtqC7xMSfj9idOnmBYyMoULfHePJyxMAw3nWhJxzc+LFwQ==} + dev: false + + /@types/d3-scale-chromatic@3.0.2: + resolution: {integrity: sha512-kpKNZMDT3OAX6b5ct5nS/mv6LULagnUy4DmS6yyNjclje1qVe7vbjPwY3q1TGz6+Wr2IUkgFatCzqYUl54fHag==} + dev: false + + /@types/d3-scale@4.0.8: + resolution: {integrity: sha512-gkK1VVTr5iNiYJ7vWDI+yUFFlszhNMtVeneJ6lUTKPjprsvLLI9/tgEGiXJOnlINJA8FyA88gfnQsHbybVZrYQ==} + dependencies: + '@types/d3-time': 3.0.3 + dev: false + + /@types/d3-selection@3.0.10: + resolution: {integrity: sha512-cuHoUgS/V3hLdjJOLTT691+G2QoqAjCVLmr4kJXR4ha56w1Zdu8UUQ5TxLRqudgNjwXeQxKMq4j+lyf9sWuslg==} + dev: false + + /@types/d3-shape@3.1.5: + resolution: {integrity: sha512-dfEWpZJ1Pdg8meLlICX1M3WBIpxnaH2eQV2eY43Y5ysRJOTAV9f3/R++lgJKFstfrEOE2zdJ0sv5qwr2Bkic6Q==} + dependencies: + '@types/d3-path': 3.0.2 + dev: false + + /@types/d3-time-format@4.0.3: + resolution: {integrity: sha512-5xg9rC+wWL8kdDj153qZcsJ0FWiFt0J5RB6LYUNZjwSnesfblqrI/bJ1wBdJ8OQfncgbJG5+2F+qfqnqyzYxyg==} + dev: false + + /@types/d3-time@3.0.3: + resolution: {integrity: sha512-2p6olUZ4w3s+07q3Tm2dbiMZy5pCDfYwtLXXHUnVzXgQlZ/OyPtUz6OL382BkOuGlLXqfT+wqv8Fw2v8/0geBw==} + dev: false + + /@types/d3-timer@3.0.2: + resolution: {integrity: sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==} + dev: false + + /@types/d3-transition@3.0.8: + resolution: {integrity: sha512-ew63aJfQ/ms7QQ4X7pk5NxQ9fZH/z+i24ZfJ6tJSfqxJMrYLiK01EAs2/Rtw/JreGUsS3pLPNV644qXFGnoZNQ==} + dependencies: + '@types/d3-selection': 3.0.10 + dev: false + + /@types/d3-zoom@3.0.8: + resolution: {integrity: sha512-iqMC4/YlFCSlO8+2Ii1GGGliCAY4XdeG748w5vQUbevlbDu0zSjH/+jojorQVBK/se0j6DUFNPBGSqD3YWYnDw==} + dependencies: + '@types/d3-interpolate': 3.0.4 + '@types/d3-selection': 3.0.10 + dev: false + + /@types/d3@7.4.3: + resolution: {integrity: sha512-lZXZ9ckh5R8uiFVt8ogUNf+pIrK4EsWrx2Np75WvF/eTpJ0FMHNhjXk8CKEx/+gpHbNQyJWehbFaTvqmHWB3ww==} + dependencies: + '@types/d3-array': 3.2.1 + '@types/d3-axis': 3.0.6 + '@types/d3-brush': 3.0.6 + '@types/d3-chord': 3.0.6 + '@types/d3-color': 3.1.3 + '@types/d3-contour': 3.0.6 + '@types/d3-delaunay': 6.0.4 + '@types/d3-dispatch': 3.0.6 + '@types/d3-drag': 3.0.7 + '@types/d3-dsv': 3.0.7 + '@types/d3-ease': 3.0.2 + '@types/d3-fetch': 3.0.7 + '@types/d3-force': 3.0.9 + '@types/d3-format': 3.0.4 + '@types/d3-geo': 3.1.0 + '@types/d3-hierarchy': 3.1.6 + '@types/d3-interpolate': 3.0.4 + '@types/d3-path': 3.0.2 + '@types/d3-polygon': 3.0.2 + '@types/d3-quadtree': 3.0.5 + '@types/d3-random': 3.0.3 + '@types/d3-scale': 4.0.8 + '@types/d3-scale-chromatic': 3.0.2 + '@types/d3-selection': 3.0.10 + '@types/d3-shape': 3.1.5 + '@types/d3-time': 3.0.3 + '@types/d3-time-format': 4.0.3 + '@types/d3-timer': 3.0.2 + '@types/d3-transition': 3.0.8 + '@types/d3-zoom': 3.0.8 + dev: false + + /@types/geojson@7946.0.13: + resolution: {integrity: sha512-bmrNrgKMOhM3WsafmbGmC+6dsF2Z308vLFsQ3a/bT8X8Sv5clVYpPars/UPq+sAaJP+5OoLAYgwbkS5QEJdLUQ==} + dev: false + + /classcat@5.0.4: + resolution: {integrity: sha512-sbpkOw6z413p+HDGcBENe498WM9woqWHiJxCq7nvmxe9WmrUmqfAcxpIwAiMtM5Q3AhYkzXcNQHqsWq0mND51g==} + dev: false + + /d3-color@3.1.0: + resolution: {integrity: sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==} + engines: {node: '>=12'} + dev: false + + /d3-dispatch@3.0.1: + resolution: {integrity: sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==} + engines: {node: '>=12'} + dev: false + + /d3-drag@3.0.0: + resolution: {integrity: sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==} + engines: {node: '>=12'} + dependencies: + d3-dispatch: 3.0.1 + d3-selection: 3.0.0 + dev: false + + /d3-ease@3.0.1: + resolution: {integrity: sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==} + engines: {node: '>=12'} + dev: false + + /d3-interpolate@3.0.1: + resolution: {integrity: sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==} + engines: {node: '>=12'} + dependencies: + d3-color: 3.1.0 + dev: false + + /d3-selection@3.0.0: + resolution: {integrity: sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==} + engines: {node: '>=12'} + dev: false + + /d3-timer@3.0.1: + resolution: {integrity: sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==} + engines: {node: '>=12'} + dev: false + + /d3-transition@3.0.1(d3-selection@3.0.0): + resolution: {integrity: sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==} + engines: {node: '>=12'} + peerDependencies: + d3-selection: 2 - 3 + dependencies: + d3-color: 3.1.0 + d3-dispatch: 3.0.1 + d3-ease: 3.0.1 + d3-interpolate: 3.0.1 + d3-selection: 3.0.0 + d3-timer: 3.0.1 + dev: false + + /d3-zoom@3.0.0: + resolution: {integrity: sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==} + engines: {node: '>=12'} + dependencies: + d3-dispatch: 3.0.1 + d3-drag: 3.0.0 + d3-interpolate: 3.0.1 + d3-selection: 3.0.0 + d3-transition: 3.0.1(d3-selection@3.0.0) + dev: false + + /js-tokens@4.0.0: + resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + dev: false + + /loose-envify@1.4.0: + resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} + hasBin: true + dependencies: + js-tokens: 4.0.0 + dev: false + + /react-dom@18.2.0(react@18.2.0): + resolution: {integrity: sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==} + peerDependencies: + react: ^18.2.0 + dependencies: + loose-envify: 1.4.0 + react: 18.2.0 + scheduler: 0.23.0 + dev: false + + /react@18.2.0: + resolution: {integrity: sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==} + engines: {node: '>=0.10.0'} + dependencies: + loose-envify: 1.4.0 + dev: false + + /scheduler@0.23.0: + resolution: {integrity: sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw==} + dependencies: + loose-envify: 1.4.0 + dev: false + + /use-sync-external-store@1.2.0(react@18.2.0): + resolution: {integrity: sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + dependencies: + react: 18.2.0 + dev: false + + /zustand@4.4.6(react@18.2.0): + resolution: {integrity: sha512-Rb16eW55gqL4W2XZpJh0fnrATxYEG3Apl2gfHTyDSE965x/zxslTikpNch0JgNjJA9zK6gEFW8Fl6d1rTZaqgg==} + engines: {node: '>=12.7.0'} + peerDependencies: + '@types/react': '>=16.8' + immer: '>=9.0' + react: '>=16.8' + peerDependenciesMeta: + '@types/react': + optional: true + immer: + optional: true + react: + optional: true + dependencies: + react: 18.2.0 + use-sync-external-store: 1.2.0(react@18.2.0) + dev: false From cf4ef116436c7c81db64ec901d6083596f1234ca Mon Sep 17 00:00:00 2001 From: ghun131 Date: Sat, 16 Dec 2023 13:20:32 +0700 Subject: [PATCH 3/5] fix: remove useAutoLayout because of redundancy --- dnd-ui/webview-ui/src/hooks/useAutoLayout.tsx | 164 ------------------ dnd-ui/webview-ui/src/pages/Flow.tsx | 7 +- 2 files changed, 1 insertion(+), 170 deletions(-) delete mode 100644 dnd-ui/webview-ui/src/hooks/useAutoLayout.tsx diff --git a/dnd-ui/webview-ui/src/hooks/useAutoLayout.tsx b/dnd-ui/webview-ui/src/hooks/useAutoLayout.tsx deleted file mode 100644 index 504df06..0000000 --- a/dnd-ui/webview-ui/src/hooks/useAutoLayout.tsx +++ /dev/null @@ -1,164 +0,0 @@ -import { useEffect } from 'react'; -import { - Node, - Edge, - Position, - ReactFlowState, - useStore, - useReactFlow, -} from 'reactflow'; -import { stratify, tree } from 'd3-hierarchy'; - -// the layout direction (T = top, R = right, B = bottom, L = left, TB = top to bottom, ...) -export type Direction = 'TB' | 'LR' | 'RL' | 'BT'; - -export type Options = { - direction: Direction; -}; - -const positionMap: Record = { - T: Position.Top, - L: Position.Left, - R: Position.Right, - B: Position.Bottom, -}; - -const getPosition = (x: number, y: number, direction: Direction) => { - switch (direction) { - case 'LR': - return { x: y, y: x }; - case 'RL': - return { x: -y, y: -x }; - case 'BT': - return { x: -x, y: -y }; - default: - return { x, y }; - } -}; - -// initialize the tree layout (see https://observablehq.com/@d3/tree for examples) -const layout = tree() - // the node size configures the spacing between the nodes ([width, height]) - .nodeSize([130, 120]) - // this is needed for creating equal space between all nodes - .separation(() => 1); - -const nodeCountSelector = (state: ReactFlowState) => state.nodeInternals.size; -const nodesInitializedSelector = (state: ReactFlowState) => - Array.from(state.nodeInternals.values()).every( - (node) => node.width && node.height - ); - -function useAutoLayout(options: Options) { - const { direction } = options; - const nodeCount = useStore(nodeCountSelector); - const nodesInitialized = useStore(nodesInitializedSelector); - const { getNodes, getEdges, setNodes, setEdges, fitView } = useReactFlow(); - - // useEffect(() => { - // // only run the layout if there are nodes and they have been initialized with their dimensions - // if (!nodeCount || !nodesInitialized) { - // return; - // } - - // const nodes: Node[] = getNodes(); - // const edges: Edge[] = getEdges(); - // console.log('edges:', edges); - // console.log('nodes:', nodes); - - // const hierarchy = stratify() - // .id((d) => d.id) - // // get the id of each node by searching through the edges - // // this only works if every node has one connection - // .parentId( - // (d: Node) => edges.find((e: Edge) => e.target === d.id)?.source - // )(nodes); - // console.log('hierarchy:', hierarchy); - - // // run the layout algorithm with the hierarchy data structure - // const root = layout(hierarchy); - - // // set the React Flow nodes with the positions from the layout - // setNodes((nodes) => - // nodes.map((node) => { - // // find the node in the hierarchy with the same id and get its coordinates - // const { x, y } = root.find((d) => d.id === node.id) || { - // x: node.position.x, - // y: node.position.y, - // }; - - // return { - // ...node, - // sourcePosition: positionMap[direction[1]], - // targetPosition: positionMap[direction[0]], - // position: getPosition(x, y, direction), - // style: { opacity: 1 }, - // }; - // }) - // ); - - // setEdges((edges) => - // edges.map((edge) => ({ ...edge, style: { opacity: 1 } })) - // ); - // }, [ - // nodeCount, - // nodesInitialized, - // getNodes, - // getEdges, - // setNodes, - // setEdges, - // fitView, - // direction, - // ]); - - function autoLayout() { - // only run the layout if there are nodes and they have been initialized with their dimensions - if (!nodeCount || !nodesInitialized) { - return; - } - - const nodes: Node[] = getNodes(); - const edges: Edge[] = getEdges(); - console.log('edges:', edges); - console.log('nodes:', nodes); - - const hierarchy = stratify() - .id((d) => d.id) - // get the id of each node by searching through the edges - // this only works if every node has one connection - .parentId( - (d: Node) => edges.find((e: Edge) => e.target === d.id)?.source - )(nodes); - console.log('hierarchy:', hierarchy); - - // run the layout algorithm with the hierarchy data structure - const root = layout(hierarchy); - - // set the React Flow nodes with the positions from the layout - setNodes((nodes) => - nodes.map((node) => { - // find the node in the hierarchy with the same id and get its coordinates - const { x, y } = root.find((d) => d.id === node.id) || { - x: node.position.x, - y: node.position.y, - }; - - return { - ...node, - sourcePosition: positionMap[direction[1]], - targetPosition: positionMap[direction[0]], - position: getPosition(x, y, direction), - style: { opacity: 1 }, - }; - }) - ); - - setEdges((edges) => - edges.map((edge) => ({ ...edge, style: { opacity: 1 } })) - ); - } - - return { autoLayout }; -} - -export default useAutoLayout; diff --git a/dnd-ui/webview-ui/src/pages/Flow.tsx b/dnd-ui/webview-ui/src/pages/Flow.tsx index 08fb776..de5fd91 100644 --- a/dnd-ui/webview-ui/src/pages/Flow.tsx +++ b/dnd-ui/webview-ui/src/pages/Flow.tsx @@ -103,7 +103,6 @@ const Editor = () => { const { height: windowHeight, width: windowWidth } = useGetWindowSize(); const { takeSnapshot, undo, redo, setPast } = useUndoRedo(); const wrapperRef = useRef(null); - // useAutoLayout({ direction: 'TB' }); const [nodes, setNodes] = useNodesState(initialNodes); const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges); @@ -121,7 +120,7 @@ const Editor = () => { const canPaste = bufferedNodes.length > 0; const viewport = useRef(defaultViewport); - const { project, getIntersectingNodes, fitView } = useReactFlow(); + const { project, getIntersectingNodes } = useReactFlow(); const rfStore = useStoreApi(); useEffect(() => { @@ -129,10 +128,6 @@ const Editor = () => { () => window.removeEventListener('message', handleCallback); }, []); - useEffect(() => { - fitView({ duration: 400 }); - }, [nodes, fitView]); - const onConnect = useCallback( (connection: Connection) => { takeSnapshot(); From 5f0e438749c62d56dcb8100b530e7fc809231e8b Mon Sep 17 00:00:00 2001 From: ghun131 Date: Sat, 16 Dec 2023 14:23:05 +0700 Subject: [PATCH 4/5] fix: increase click button area --- dnd-ui/webview-ui/src/components/NodeMenuPanel.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/dnd-ui/webview-ui/src/components/NodeMenuPanel.tsx b/dnd-ui/webview-ui/src/components/NodeMenuPanel.tsx index a54609f..08076c8 100644 --- a/dnd-ui/webview-ui/src/components/NodeMenuPanel.tsx +++ b/dnd-ui/webview-ui/src/components/NodeMenuPanel.tsx @@ -78,6 +78,7 @@ function NodeMenuPanel({ > + {hasChildNodes && (