mirror of
				https://github.com/lingble/twenty.git
				synced 2025-10-30 20:27:55 +00:00 
			
		
		
		
	Improve the layout of the Workflow Visualizer (#8372)
- Increase the dimensions of the ReactFlow nodes. This allows to ditch scaling which made it hard to get the width of the nodes as they were visually scaled by 1.3. - Center the flow when the flow mounts and when the state of the right drawer opens. - Put the node type inside of the node so it doesn't overlap with the arrow - Make the edges non deletable We'll have to make a refactor so the viewport can be animated properly: https://github.com/twentyhq/twenty/issues/8387. https://github.com/user-attachments/assets/69494a32-5403-4898-be75-7fc38058e263 --------- Co-authored-by: Félix Malfait <felix@twenty.com>
This commit is contained in:
		 Baptiste Devessier
					Baptiste Devessier
				
			
				
					committed by
					
						 GitHub
						GitHub
					
				
			
			
				
	
			
			
			 GitHub
						GitHub
					
				
			
						parent
						
							aadcb49dcb
						
					
				
				
					commit
					31f03764d6
				
			| @@ -21,13 +21,12 @@ const StyledStepNodeType = styled.div` | ||||
|     ${({ theme }) => theme.border.radius.sm} 0 0; | ||||
|  | ||||
|   color: ${({ theme }) => theme.color.gray50}; | ||||
|   font-size: ${({ theme }) => theme.font.size.xs}; | ||||
|   font-size: ${({ theme }) => theme.font.size.md}; | ||||
|   font-weight: ${({ theme }) => theme.font.weight.semiBold}; | ||||
|  | ||||
|   margin-left: ${({ theme }) => theme.spacing(2)}; | ||||
|   padding: ${({ theme }) => theme.spacing(1)} ${({ theme }) => theme.spacing(2)}; | ||||
|   position: absolute; | ||||
|   top: 0; | ||||
|   transform: translateY(-100%); | ||||
|   align-self: flex-start; | ||||
|  | ||||
|   .selectable.selected &, | ||||
|   .selectable:focus &, | ||||
| @@ -62,9 +61,9 @@ const StyledStepNodeInnerContainer = styled.div<{ variant?: Variant }>` | ||||
| const StyledStepNodeLabel = styled.div<{ variant?: Variant }>` | ||||
|   align-items: center; | ||||
|   display: flex; | ||||
|   font-size: ${({ theme }) => theme.font.size.md}; | ||||
|   font-size: ${({ theme }) => theme.font.size.lg}; | ||||
|   font-weight: ${({ theme }) => theme.font.weight.medium}; | ||||
|   column-gap: ${({ theme }) => theme.spacing(2)}; | ||||
|   column-gap: ${({ theme }) => theme.spacing(3)}; | ||||
|   color: ${({ variant, theme }) => | ||||
|     variant === 'placeholder' | ||||
|       ? theme.font.color.extraLight | ||||
| @@ -80,9 +79,13 @@ export const StyledTargetHandle = styled(Handle)` | ||||
| `; | ||||
|  | ||||
| const StyledRightFloatingElementContainer = styled.div` | ||||
|   display: flex; | ||||
|   align-items: center; | ||||
|   position: absolute; | ||||
|   right: ${({ theme }) => theme.spacing(-3)}; | ||||
|   bottom: 0; | ||||
|   top: 0; | ||||
|   transform: translateX(100%); | ||||
|   right: ${({ theme }) => theme.spacing(-2)}; | ||||
| `; | ||||
|  | ||||
| export const WorkflowDiagramBaseStepNode = ({ | ||||
| @@ -104,9 +107,9 @@ export const WorkflowDiagramBaseStepNode = ({ | ||||
|         <StyledTargetHandle type="target" position={Position.Top} /> | ||||
|       ) : null} | ||||
|  | ||||
|       <StyledStepNodeInnerContainer variant={variant}> | ||||
|       <StyledStepNodeType>{capitalize(nodeType)}</StyledStepNodeType> | ||||
|  | ||||
|       <StyledStepNodeInnerContainer variant={variant}> | ||||
|         <StyledStepNodeLabel variant={variant}> | ||||
|           {Icon} | ||||
|  | ||||
|   | ||||
| @@ -1,3 +1,6 @@ | ||||
| import { isRightDrawerMinimizedState } from '@/ui/layout/right-drawer/states/isRightDrawerMinimizedState'; | ||||
| import { isRightDrawerOpenState } from '@/ui/layout/right-drawer/states/isRightDrawerOpenState'; | ||||
| import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile'; | ||||
| import { WorkflowVersionStatusTag } from '@/workflow/components/WorkflowVersionStatusTag'; | ||||
| import { workflowDiagramState } from '@/workflow/states/workflowDiagramState'; | ||||
| import { WorkflowVersionStatus } from '@/workflow/types/Workflow'; | ||||
| @@ -15,14 +18,16 @@ import { | ||||
|   Background, | ||||
|   EdgeChange, | ||||
|   FitViewOptions, | ||||
|   getNodesBounds, | ||||
|   NodeChange, | ||||
|   NodeProps, | ||||
|   ReactFlow, | ||||
|   useReactFlow, | ||||
| } from '@xyflow/react'; | ||||
| import '@xyflow/react/dist/style.css'; | ||||
| import React, { useMemo } from 'react'; | ||||
| import { useSetRecoilState } from 'recoil'; | ||||
| import { GRAY_SCALE, isDefined } from 'twenty-ui'; | ||||
| import React, { useEffect, useMemo, useRef } from 'react'; | ||||
| import { useRecoilValue, useSetRecoilState } from 'recoil'; | ||||
| import { GRAY_SCALE, isDefined, THEME_COMMON } from 'twenty-ui'; | ||||
|  | ||||
| const StyledResetReactflowStyles = styled.div` | ||||
|   height: 100%; | ||||
| @@ -35,6 +40,9 @@ const StyledResetReactflowStyles = styled.div` | ||||
|   .react-flow__node-output, | ||||
|   .react-flow__node-group { | ||||
|     padding: 0; | ||||
|     width: auto; | ||||
|     text-align: start; | ||||
|     white-space: nowrap; | ||||
|   } | ||||
|  | ||||
|   --xy-node-border-radius: none; | ||||
| @@ -51,10 +59,10 @@ const StyledStatusTagContainer = styled.div` | ||||
|   padding: ${({ theme }) => theme.spacing(2)}; | ||||
| `; | ||||
|  | ||||
| const defaultFitViewOptions: FitViewOptions = { | ||||
|   minZoom: 1.3, | ||||
|   maxZoom: 1.3, | ||||
| }; | ||||
| const defaultFitViewOptions = { | ||||
|   minZoom: 1, | ||||
|   maxZoom: 1, | ||||
| } satisfies FitViewOptions; | ||||
|  | ||||
| export const WorkflowDiagramCanvasBase = ({ | ||||
|   diagram, | ||||
| @@ -77,11 +85,29 @@ export const WorkflowDiagramCanvasBase = ({ | ||||
|   >; | ||||
|   children?: React.ReactNode; | ||||
| }) => { | ||||
|   const reactflow = useReactFlow(); | ||||
|  | ||||
|   const { nodes, edges } = useMemo( | ||||
|     () => getOrganizedDiagram(diagram), | ||||
|     [diagram], | ||||
|   ); | ||||
|  | ||||
|   const isRightDrawerOpen = useRecoilValue(isRightDrawerOpenState); | ||||
|   const isRightDrawerMinimized = useRecoilValue(isRightDrawerMinimizedState); | ||||
|   const isMobile = useIsMobile(); | ||||
|  | ||||
|   const rightDrawerState = !isRightDrawerOpen | ||||
|     ? 'closed' | ||||
|     : isRightDrawerMinimized | ||||
|       ? 'minimized' | ||||
|       : isMobile | ||||
|         ? 'fullScreen' | ||||
|         : 'normal'; | ||||
|  | ||||
|   const rightDrawerWidth = Number( | ||||
|     THEME_COMMON.rightDrawerWidth.replace('px', ''), | ||||
|   ); | ||||
|  | ||||
|   const setWorkflowDiagram = useSetRecoilState(workflowDiagramState); | ||||
|  | ||||
|   const handleNodesChange = ( | ||||
| @@ -118,27 +144,68 @@ export const WorkflowDiagramCanvasBase = ({ | ||||
|     }); | ||||
|   }; | ||||
|  | ||||
|   const containerRef = useRef<HTMLDivElement>(null); | ||||
|  | ||||
|   useEffect(() => { | ||||
|     if (!isDefined(containerRef.current) || !reactflow.viewportInitialized) { | ||||
|       return; | ||||
|     } | ||||
|  | ||||
|     const currentViewport = reactflow.getViewport(); | ||||
|  | ||||
|     const flowBounds = getNodesBounds(reactflow.getNodes()); | ||||
|  | ||||
|     let visibleRightDrawerWidth = 0; | ||||
|     if (rightDrawerState === 'normal') { | ||||
|       visibleRightDrawerWidth = rightDrawerWidth; | ||||
|     } | ||||
|  | ||||
|     const viewportX = | ||||
|       (containerRef.current.offsetWidth + visibleRightDrawerWidth) / 2 - | ||||
|       flowBounds.width / 2; | ||||
|  | ||||
|     reactflow.setViewport( | ||||
|       { | ||||
|         ...currentViewport, | ||||
|         x: viewportX - visibleRightDrawerWidth, | ||||
|       }, | ||||
|       { duration: 300 }, | ||||
|     ); | ||||
|   }, [reactflow, rightDrawerState, rightDrawerWidth]); | ||||
|  | ||||
|   return ( | ||||
|     <StyledResetReactflowStyles> | ||||
|     <StyledResetReactflowStyles ref={containerRef}> | ||||
|       <ReactFlow | ||||
|         onInit={({ fitView }) => { | ||||
|           fitView(defaultFitViewOptions); | ||||
|         onInit={() => { | ||||
|           if (!isDefined(containerRef.current)) { | ||||
|             throw new Error('Expect the container ref to be defined'); | ||||
|           } | ||||
|  | ||||
|           const flowBounds = getNodesBounds(reactflow.getNodes()); | ||||
|  | ||||
|           reactflow.setViewport({ | ||||
|             x: containerRef.current.offsetWidth / 2 - flowBounds.width / 2, | ||||
|             y: 150, | ||||
|             zoom: defaultFitViewOptions.maxZoom, | ||||
|           }); | ||||
|         }} | ||||
|         minZoom={defaultFitViewOptions.minZoom} | ||||
|         maxZoom={defaultFitViewOptions.maxZoom} | ||||
|         nodeTypes={nodeTypes} | ||||
|         fitView | ||||
|         nodes={nodes.map((node) => ({ ...node, draggable: false }))} | ||||
|         edges={edges} | ||||
|         onNodesChange={handleNodesChange} | ||||
|         onEdgesChange={handleEdgesChange} | ||||
|         proOptions={{ hideAttribution: true }} | ||||
|       > | ||||
|         <Background color={GRAY_SCALE.gray25} size={2} /> | ||||
|  | ||||
|         {children} | ||||
|       </ReactFlow> | ||||
|  | ||||
|       <StyledStatusTagContainer> | ||||
|         <WorkflowVersionStatusTag versionStatus={status} /> | ||||
|       </StyledStatusTagContainer> | ||||
|       </ReactFlow> | ||||
|     </StyledResetReactflowStyles> | ||||
|   ); | ||||
| }; | ||||
|   | ||||
| @@ -5,6 +5,7 @@ import { WorkflowDiagramEmptyTrigger } from '@/workflow/components/WorkflowDiagr | ||||
| import { WorkflowDiagramStepNodeEditable } from '@/workflow/components/WorkflowDiagramStepNodeEditable'; | ||||
| import { WorkflowWithCurrentVersion } from '@/workflow/types/Workflow'; | ||||
| import { WorkflowDiagram } from '@/workflow/types/WorkflowDiagram'; | ||||
| import { ReactFlowProvider } from '@xyflow/react'; | ||||
|  | ||||
| export const WorkflowDiagramCanvasEditable = ({ | ||||
|   diagram, | ||||
| @@ -14,6 +15,7 @@ export const WorkflowDiagramCanvasEditable = ({ | ||||
|   workflowWithCurrentVersion: WorkflowWithCurrentVersion; | ||||
| }) => { | ||||
|   return ( | ||||
|     <ReactFlowProvider> | ||||
|       <WorkflowDiagramCanvasBase | ||||
|         key={workflowWithCurrentVersion.currentVersion.id} | ||||
|         diagram={diagram} | ||||
| @@ -26,5 +28,6 @@ export const WorkflowDiagramCanvasEditable = ({ | ||||
|       > | ||||
|         <WorkflowDiagramCanvasEditableEffect /> | ||||
|       </WorkflowDiagramCanvasBase> | ||||
|     </ReactFlowProvider> | ||||
|   ); | ||||
| }; | ||||
|   | ||||
| @@ -4,6 +4,7 @@ import { WorkflowDiagramEmptyTrigger } from '@/workflow/components/WorkflowDiagr | ||||
| import { WorkflowDiagramStepNodeReadonly } from '@/workflow/components/WorkflowDiagramStepNodeReadonly'; | ||||
| import { WorkflowVersion } from '@/workflow/types/Workflow'; | ||||
| import { WorkflowDiagram } from '@/workflow/types/WorkflowDiagram'; | ||||
| import { ReactFlowProvider } from '@xyflow/react'; | ||||
|  | ||||
| export const WorkflowDiagramCanvasReadonly = ({ | ||||
|   diagram, | ||||
| @@ -13,6 +14,7 @@ export const WorkflowDiagramCanvasReadonly = ({ | ||||
|   workflowVersion: WorkflowVersion; | ||||
| }) => { | ||||
|   return ( | ||||
|     <ReactFlowProvider> | ||||
|       <WorkflowDiagramCanvasBase | ||||
|         key={workflowVersion.id} | ||||
|         diagram={diagram} | ||||
| @@ -24,5 +26,6 @@ export const WorkflowDiagramCanvasReadonly = ({ | ||||
|       > | ||||
|         <WorkflowDiagramCanvasReadonlyEffect /> | ||||
|       </WorkflowDiagramCanvasBase> | ||||
|     </ReactFlowProvider> | ||||
|   ); | ||||
| }; | ||||
|   | ||||
| @@ -11,7 +11,7 @@ export const WorkflowDiagramCreateStepNode = () => { | ||||
|     <> | ||||
|       <StyledTargetHandle type="target" position={Position.Top} /> | ||||
|  | ||||
|       <IconButton Icon={IconPlus} size="small" /> | ||||
|       <IconButton Icon={IconPlus} size="medium" /> | ||||
|     </> | ||||
|   ); | ||||
| }; | ||||
|   | ||||
| @@ -31,7 +31,7 @@ export const WorkflowDiagramStepNodeBase = ({ | ||||
|             return ( | ||||
|               <StyledStepNodeLabelIconContainer> | ||||
|                 <IconPlaylistAdd | ||||
|                   size={theme.icon.size.sm} | ||||
|                   size={theme.icon.size.lg} | ||||
|                   color={theme.font.color.tertiary} | ||||
|                 /> | ||||
|               </StyledStepNodeLabelIconContainer> | ||||
| @@ -41,7 +41,7 @@ export const WorkflowDiagramStepNodeBase = ({ | ||||
|             return ( | ||||
|               <StyledStepNodeLabelIconContainer> | ||||
|                 <IconHandMove | ||||
|                   size={theme.icon.size.sm} | ||||
|                   size={theme.icon.size.lg} | ||||
|                   color={theme.font.color.tertiary} | ||||
|                 /> | ||||
|               </StyledStepNodeLabelIconContainer> | ||||
| @@ -57,7 +57,7 @@ export const WorkflowDiagramStepNodeBase = ({ | ||||
|             return ( | ||||
|               <StyledStepNodeLabelIconContainer> | ||||
|                 <IconCode | ||||
|                   size={theme.icon.size.sm} | ||||
|                   size={theme.icon.size.lg} | ||||
|                   color={theme.color.orange} | ||||
|                 /> | ||||
|               </StyledStepNodeLabelIconContainer> | ||||
| @@ -66,7 +66,7 @@ export const WorkflowDiagramStepNodeBase = ({ | ||||
|           case 'SEND_EMAIL': { | ||||
|             return ( | ||||
|               <StyledStepNodeLabelIconContainer> | ||||
|                 <IconMail size={theme.icon.size.sm} color={theme.color.blue} /> | ||||
|                 <IconMail size={theme.icon.size.lg} color={theme.color.blue} /> | ||||
|               </StyledStepNodeLabelIconContainer> | ||||
|             ); | ||||
|           } | ||||
|   | ||||
| @@ -32,6 +32,7 @@ export const WorkflowDiagramStepNodeEditable = ({ | ||||
|       RightFloatingElement={ | ||||
|         selected ? ( | ||||
|           <FloatingIconButton | ||||
|             size="medium" | ||||
|             Icon={IconTrash} | ||||
|             onClick={() => { | ||||
|               return deleteOneStep(); | ||||
|   | ||||
| @@ -36,6 +36,7 @@ export const addCreateStepNodes = ({ nodes, edges }: WorkflowDiagram) => { | ||||
|       markerEnd: { | ||||
|         type: MarkerType.ArrowClosed, | ||||
|       }, | ||||
|       deletable: false, | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   | ||||
| @@ -51,6 +51,7 @@ export const generateWorkflowDiagram = ({ | ||||
|       markerEnd: { | ||||
|         type: MarkerType.ArrowClosed, | ||||
|       }, | ||||
|       deletable: false, | ||||
|     }); | ||||
|  | ||||
|     return nodeId; | ||||
|   | ||||
		Reference in New Issue
	
	Block a user