Feature: extends ReactFlow controls with snapping functionality (#4482)

* Feature: extends ReactFlow controls with snapping functionality

* Adds snapping on other flows

* lint fix, add dark mode, fix marketplace canvas

---------

Co-authored-by: Corentin <corentin.hoareau@sogeti.com>
Co-authored-by: Henry <hzj94@hotmail.com>
This commit is contained in:
corentin-hoareau 2025-05-27 20:54:33 +02:00 committed by GitHub
parent 4326cbe6b5
commit 82d60c7d15
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 158 additions and 11 deletions

View File

@ -42,7 +42,7 @@ import useApi from '@/hooks/useApi'
import useConfirm from '@/hooks/useConfirm'
// icons
import { IconX, IconRefreshAlert } from '@tabler/icons-react'
import { IconX, IconRefreshAlert, IconMagnetFilled, IconMagnetOff } from '@tabler/icons-react'
// utils
import {
@ -100,6 +100,7 @@ const AgentflowCanvas = () => {
const [isSyncNodesButtonEnabled, setIsSyncNodesButtonEnabled] = useState(false)
const [editNodeDialogOpen, setEditNodeDialogOpen] = useState(false)
const [editNodeDialogProps, setEditNodeDialogProps] = useState({})
const [isSnappingEnabled, setIsSnappingEnabled] = useState(false)
const reactFlowWrapper = useRef(null)
@ -718,17 +719,30 @@ const AgentflowCanvas = () => {
fitView
deleteKeyCode={canvas.canvasDialogShow ? null : ['Delete']}
minZoom={0.5}
snapGrid={[25, 25]}
snapToGrid={isSnappingEnabled}
connectionLineComponent={ConnectionLine}
>
<Controls
className={customization.isDarkMode ? 'dark-mode-controls' : ''}
style={{
display: 'flex',
flexDirection: 'row',
left: '50%',
transform: 'translate(-50%, -50%)',
backgroundColor: customization.isDarkMode ? theme.palette.background.default : '#fff'
transform: 'translate(-50%, -50%)'
}}
/>
>
<button
className='react-flow__controls-button react-flow__controls-interactive'
onClick={() => {
setIsSnappingEnabled(!isSnappingEnabled)
}}
title='toggle snapping'
aria-label='toggle snapping'
>
{isSnappingEnabled ? <IconMagnetFilled /> : <IconMagnetOff />}
</button>
</Controls>
<MiniMap
nodeStrokeWidth={3}
nodeColor={customization.isDarkMode ? '#2d2d2d' : '#e2e2e2'}

View File

@ -4,6 +4,7 @@ import 'reactflow/dist/style.css'
import '@/views/canvas/index.css'
import { useLocation, useNavigate } from 'react-router-dom'
import { useSelector } from 'react-redux'
// material-ui
import { Toolbar, Box, AppBar } from '@mui/material'
@ -18,6 +19,9 @@ import StickyNote from './StickyNote'
import EditNodeDialog from '@/views/agentflowsv2/EditNodeDialog'
import { flowContext } from '@/store/context/ReactFlowContext'
// icons
import { IconMagnetFilled, IconMagnetOff } from '@tabler/icons-react'
const nodeTypes = { agentFlow: AgentFlowNode, stickyNote: StickyNote, iteration: IterationNode }
const edgeTypes = { agentFlow: AgentFlowEdge }
@ -26,6 +30,7 @@ const edgeTypes = { agentFlow: AgentFlowEdge }
const MarketplaceCanvasV2 = () => {
const theme = useTheme()
const navigate = useNavigate()
const customization = useSelector((state) => state.customization)
const { state } = useLocation()
const { flowData, name } = state
@ -36,6 +41,7 @@ const MarketplaceCanvasV2 = () => {
const [edges, setEdges, onEdgesChange] = useEdgesState()
const [editNodeDialogOpen, setEditNodeDialogOpen] = useState(false)
const [editNodeDialogProps, setEditNodeDialogProps] = useState({})
const [isSnappingEnabled, setIsSnappingEnabled] = useState(false)
const reactFlowWrapper = useRef(null)
const { setReactFlowInstance } = useContext(flowContext)
@ -108,15 +114,29 @@ const MarketplaceCanvasV2 = () => {
edgeTypes={edgeTypes}
fitView
minZoom={0.1}
snapGrid={[25, 25]}
snapToGrid={isSnappingEnabled}
>
<Controls
className={customization.isDarkMode ? 'dark-mode-controls' : ''}
style={{
display: 'flex',
flexDirection: 'row',
left: '50%',
transform: 'translate(-50%, -50%)'
}}
/>
>
<button
className='react-flow__controls-button react-flow__controls-interactive'
onClick={() => {
setIsSnappingEnabled(!isSnappingEnabled)
}}
title='toggle snapping'
aria-label='toggle snapping'
>
{isSnappingEnabled ? <IconMagnetFilled /> : <IconMagnetOff />}
</button>
</Controls>
<Background color='#aaa' gap={16} />
<EditNodeDialog
show={editNodeDialogOpen}

View File

@ -54,3 +54,42 @@
stroke-width: 3 !important;
opacity: 1;
}
/* Dark mode controls styling */
.dark-mode-controls {
--xy-controls-button-background-color-default: #2d2d2d;
--xy-controls-button-background-color-hover-default: #404040;
--xy-controls-button-border-color-default: #525252;
--xy-controls-box-shadow-default: 0 0 2px 1px rgba(255, 255, 255, 0.1);
}
.dark-mode-controls .react-flow__controls-button {
background-color: #2d2d2d;
border-color: #525252;
color: #ffffff;
border: 1px solid #525252;
}
.dark-mode-controls .react-flow__controls-button:hover {
background-color: #404040;
}
.dark-mode-controls .react-flow__controls-button.react-flow__controls-interactive {
background-color: #2d2d2d;
border-color: #525252;
color: #ffffff;
}
.dark-mode-controls .react-flow__controls-button.react-flow__controls-interactive:hover {
background-color: #404040;
}
.dark-mode-controls .react-flow__controls-button svg {
color: #ffffff;
fill: #ffffff;
}
.dark-mode-controls .react-flow__controls-button:hover svg {
color: #ffffff;
fill: #ffffff;
}

View File

@ -47,3 +47,42 @@
cursor: crosshair;
background: #5dba62 !important;
}
/* Dark mode controls styling */
.dark-mode-controls {
--xy-controls-button-background-color-default: #2d2d2d;
--xy-controls-button-background-color-hover-default: #404040;
--xy-controls-button-border-color-default: #525252;
--xy-controls-box-shadow-default: 0 0 2px 1px rgba(255, 255, 255, 0.1);
}
.dark-mode-controls .react-flow__controls-button {
background-color: #2d2d2d;
border-color: #525252;
color: #ffffff;
border: 1px solid #525252;
}
.dark-mode-controls .react-flow__controls-button:hover {
background-color: #404040;
}
.dark-mode-controls .react-flow__controls-button.react-flow__controls-interactive {
background-color: #2d2d2d;
border-color: #525252;
color: #ffffff;
}
.dark-mode-controls .react-flow__controls-button.react-flow__controls-interactive:hover {
background-color: #404040;
}
.dark-mode-controls .react-flow__controls-button svg {
color: #ffffff;
fill: #ffffff;
}
.dark-mode-controls .react-flow__controls-button:hover svg {
color: #ffffff;
fill: #ffffff;
}

View File

@ -38,7 +38,7 @@ import useConfirm from '@/hooks/useConfirm'
import { useAuth } from '@/hooks/useAuth'
// icons
import { IconX, IconRefreshAlert } from '@tabler/icons-react'
import { IconX, IconRefreshAlert, IconMagnetFilled, IconMagnetOff } from '@tabler/icons-react'
// utils
import {
@ -77,6 +77,7 @@ const Canvas = () => {
const { confirm } = useConfirm()
const dispatch = useDispatch()
const customization = useSelector((state) => state.customization)
const canvas = useSelector((state) => state.canvas)
const [canvasDataStore, setCanvasDataStore] = useState(canvas)
const [chatflow, setChatflow] = useState(null)
@ -96,6 +97,7 @@ const Canvas = () => {
const [selectedNode, setSelectedNode] = useState(null)
const [isUpsertButtonEnabled, setIsUpsertButtonEnabled] = useState(false)
const [isSyncNodesButtonEnabled, setIsSyncNodesButtonEnabled] = useState(false)
const [isSnappingEnabled, setIsSnappingEnabled] = useState(false)
const reactFlowWrapper = useRef(null)
@ -596,16 +598,30 @@ const Canvas = () => {
fitView
deleteKeyCode={canvas.canvasDialogShow ? null : ['Delete']}
minZoom={0.1}
snapGrid={[25, 25]}
snapToGrid={isSnappingEnabled}
className='chatflow-canvas'
>
<Controls
className={customization.isDarkMode ? 'dark-mode-controls' : ''}
style={{
display: 'flex',
flexDirection: 'row',
left: '50%',
transform: 'translate(-50%, -50%)'
}}
/>
>
<button
className='react-flow__controls-button react-flow__controls-interactive'
onClick={() => {
setIsSnappingEnabled(!isSnappingEnabled)
}}
title='toggle snapping'
aria-label='toggle snapping'
>
{isSnappingEnabled ? <IconMagnetFilled /> : <IconMagnetOff />}
</button>
</Controls>
<Background color='#aaa' gap={16} />
<AddNodes isAgentCanvas={isAgentCanvas} nodesData={getNodesApi.data} node={selectedNode} />
{isSyncNodesButtonEnabled && (

View File

@ -1,9 +1,10 @@
import { useEffect, useRef } from 'react'
import { useEffect, useRef, useState } from 'react'
import ReactFlow, { Controls, Background, useNodesState, useEdgesState } from 'reactflow'
import 'reactflow/dist/style.css'
import '@/views/canvas/index.css'
import { useLocation, useNavigate } from 'react-router-dom'
import { useSelector } from 'react-redux'
// material-ui
import { Toolbar, Box, AppBar } from '@mui/material'
@ -14,6 +15,9 @@ import MarketplaceCanvasNode from './MarketplaceCanvasNode'
import MarketplaceCanvasHeader from './MarketplaceCanvasHeader'
import StickyNote from '../canvas/StickyNote'
// icons
import { IconMagnetFilled, IconMagnetOff } from '@tabler/icons-react'
const nodeTypes = { customNode: MarketplaceCanvasNode, stickyNote: StickyNote }
const edgeTypes = { buttonedge: '' }
@ -22,15 +26,16 @@ const edgeTypes = { buttonedge: '' }
const MarketplaceCanvas = () => {
const theme = useTheme()
const navigate = useNavigate()
const customization = useSelector((state) => state.customization)
const { state } = useLocation()
const flowData = state?.flowData || '{}'
const name = state?.name || 'Untitled'
const { flowData, name } = state
// ==============================|| ReactFlow ||============================== //
const [nodes, setNodes, onNodesChange] = useNodesState()
const [edges, setEdges, onEdgesChange] = useEdgesState()
const [isSnappingEnabled, setIsSnappingEnabled] = useState(false)
const reactFlowWrapper = useRef(null)
@ -87,15 +92,29 @@ const MarketplaceCanvas = () => {
edgeTypes={edgeTypes}
fitView
minZoom={0.1}
snapGrid={[25, 25]}
snapToGrid={isSnappingEnabled}
>
<Controls
className={customization.isDarkMode ? 'dark-mode-controls' : ''}
style={{
display: 'flex',
flexDirection: 'row',
left: '50%',
transform: 'translate(-50%, -50%)'
}}
/>
>
<button
className='react-flow__controls-button react-flow__controls-interactive'
onClick={() => {
setIsSnappingEnabled(!isSnappingEnabled)
}}
title='toggle snapping'
aria-label='toggle snapping'
>
{isSnappingEnabled ? <IconMagnetFilled /> : <IconMagnetOff />}
</button>
</Controls>
<Background color='#aaa' gap={16} />
</ReactFlow>
</div>