-
Notifications
You must be signed in to change notification settings - Fork 2.2k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Improve the layout of the Workflow Visualizer #8372
Changes from all commits
ac60872
bc1c2e5
57238c5
f9ff8d1
595fdfa
5fef78d
04ac163
9d7ca61
ee99150
c46c6d5
13aeb8a
9b0c422
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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, | ||
}, | ||
Comment on lines
+163
to
+171
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. logic: viewport calculation may cause layout shift when drawer width changes - consider caching previous drawer state to prevent unnecessary recalculations |
||
{ 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, | ||
}); | ||
}} | ||
Comment on lines
+179
to
191
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. logic: onInit runs before nodes are measured, which could cause incorrect initial positioning |
||
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} | ||
|
||
<StyledStatusTagContainer> | ||
<WorkflowVersionStatusTag versionStatus={status} /> | ||
</StyledStatusTagContainer> | ||
</ReactFlow> | ||
|
||
<StyledStatusTagContainer> | ||
<WorkflowVersionStatusTag versionStatus={status} /> | ||
</StyledStatusTagContainer> | ||
</StyledResetReactflowStyles> | ||
); | ||
}; |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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,17 +15,19 @@ export const WorkflowDiagramCanvasEditable = ({ | |
workflowWithCurrentVersion: WorkflowWithCurrentVersion; | ||
}) => { | ||
return ( | ||
<WorkflowDiagramCanvasBase | ||
key={workflowWithCurrentVersion.currentVersion.id} | ||
diagram={diagram} | ||
status={workflowWithCurrentVersion.currentVersion.status} | ||
nodeTypes={{ | ||
default: WorkflowDiagramStepNodeEditable, | ||
'create-step': WorkflowDiagramCreateStepNode, | ||
'empty-trigger': WorkflowDiagramEmptyTrigger, | ||
}} | ||
> | ||
<WorkflowDiagramCanvasEditableEffect /> | ||
</WorkflowDiagramCanvasBase> | ||
<ReactFlowProvider> | ||
<WorkflowDiagramCanvasBase | ||
key={workflowWithCurrentVersion.currentVersion.id} | ||
diagram={diagram} | ||
status={workflowWithCurrentVersion.currentVersion.status} | ||
nodeTypes={{ | ||
default: WorkflowDiagramStepNodeEditable, | ||
'create-step': WorkflowDiagramCreateStepNode, | ||
'empty-trigger': WorkflowDiagramEmptyTrigger, | ||
}} | ||
> | ||
<WorkflowDiagramCanvasEditableEffect /> | ||
</WorkflowDiagramCanvasBase> | ||
</ReactFlowProvider> | ||
Comment on lines
+18
to
+31
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. style: ReactFlowProvider should be at a higher level in the component tree to avoid re-initializing the flow context on re-renders |
||
); | ||
}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
logic: missing cleanup function in useEffect - could cause memory leaks if component unmounts during animation