Skip to content

Commit 319e7e6

Browse files
Devessierehconitin
authored andcommitted
Set viewport when nodes dimensions are ready (twentyhq#12730)
Sometimes, we try to set the viewport, but the nodes' dimensions have been reset. Trying to set the viewport when the nodes' dimensions are incorrect leads to an incorrect viewport. This PR ensures we only try to set the viewport if the nodes' dimensions are valid. Otherwise, we wait for them to be computed to set the viewport automatically. The `handleNodesChanges` function is called every time the nodes change, including when the dimensions have been computed. Internally, Reactflow has a similar behavior to implement the `fitView` feature: https://github.com/xyflow/xyflow/blob/f9971a8fad54e9c2f33b71b4056b6d1ec6c33bd1/packages/react/src/store/index.ts#L111. ## Example This is more notable since I added optimistic rendering to workflow runs. https://github.com/user-attachments/assets/07232050-b808-4345-b82b-95acad72ab15
1 parent 48ffe65 commit 319e7e6

File tree

2 files changed

+175
-87
lines changed

2 files changed

+175
-87
lines changed

packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowDiagramCanvasBase.tsx

Lines changed: 166 additions & 87 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,23 @@
11
import { ActionMenuContext } from '@/action-menu/contexts/ActionMenuContext';
22
import { CommandMenuAnimationVariant } from '@/command-menu/types/CommandMenuAnimationVariant';
33
import { useListenRightDrawerClose } from '@/ui/layout/right-drawer/hooks/useListenRightDrawerClose';
4+
import { getSnapshotValue } from '@/ui/utilities/recoil-scope/utils/getSnapshotValue';
5+
import { useRecoilComponentCallbackStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackStateV2';
46
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
57
import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2';
68
import { WorkflowDiagramCustomMarkers } from '@/workflow/workflow-diagram/components/WorkflowDiagramCustomMarkers';
79
import { useRightDrawerState } from '@/workflow/workflow-diagram/hooks/useRightDrawerState';
810
import { workflowDiagramComponentState } from '@/workflow/workflow-diagram/states/workflowDiagramComponentState';
11+
import { workflowDiagramWaitingNodesDimensionsComponentState } from '@/workflow/workflow-diagram/states/workflowDiagramWaitingNodesDimensionsComponentState';
912
import {
13+
WorkflowDiagram,
1014
WorkflowDiagramEdge,
1115
WorkflowDiagramEdgeType,
1216
WorkflowDiagramNode,
1317
WorkflowDiagramNodeType,
1418
} from '@/workflow/workflow-diagram/types/WorkflowDiagram';
1519
import { getOrganizedDiagram } from '@/workflow/workflow-diagram/utils/getOrganizedDiagram';
20+
import { workflowInsertStepIdsComponentState } from '@/workflow/workflow-steps/states/workflowInsertStepIdsComponentState';
1621
import { useTheme } from '@emotion/react';
1722
import styled from '@emotion/styled';
1823
import {
@@ -28,18 +33,11 @@ import {
2833
useReactFlow,
2934
} from '@xyflow/react';
3035
import '@xyflow/react/dist/style.css';
31-
import React, {
32-
useCallback,
33-
useContext,
34-
useEffect,
35-
useMemo,
36-
useRef,
37-
useState,
38-
} from 'react';
36+
import React, { useContext, useEffect, useMemo, useRef, useState } from 'react';
37+
import { useRecoilCallback } from 'recoil';
3938
import { isDefined } from 'twenty-shared/utils';
4039
import { Tag, TagColor } from 'twenty-ui/components';
4140
import { THEME_COMMON } from 'twenty-ui/theme';
42-
import { workflowInsertStepIdsComponentState } from '@/workflow/workflow-steps/states/workflowInsertStepIdsComponentState';
4341

4442
const StyledResetReactflowStyles = styled.div`
4543
height: 100%;
@@ -134,15 +132,25 @@ export const WorkflowDiagramCanvasBase = ({
134132
const workflowDiagram = useRecoilComponentValueV2(
135133
workflowDiagramComponentState,
136134
);
137-
135+
const workflowDiagramState = useRecoilComponentCallbackStateV2(
136+
workflowDiagramComponentState,
137+
);
138+
const setWorkflowDiagram = useSetRecoilComponentStateV2(
139+
workflowDiagramComponentState,
140+
);
138141
const setWorkflowInsertStepIds = useSetRecoilComponentStateV2(
139142
workflowInsertStepIdsComponentState,
140143
);
144+
const workflowDiagramWaitingNodesDimensionsState =
145+
useRecoilComponentCallbackStateV2(
146+
workflowDiagramWaitingNodesDimensionsComponentState,
147+
);
148+
const setWorkflowDiagramWaitingNodesDimensions = useSetRecoilComponentStateV2(
149+
workflowDiagramWaitingNodesDimensionsComponentState,
150+
);
141151

142-
const [
143-
workflowDiagramFlowInitializationStatus,
144-
setWorkflowDiagramFlowInitializationStatus,
145-
] = useState<'not-initialized' | 'initialized'>('not-initialized');
152+
const [workflowDiagramFlowInitialized, setWorkflowDiagramFlowInitialized] =
153+
useState<boolean>(false);
146154

147155
const { nodes, edges } = useMemo(
148156
() =>
@@ -155,10 +163,6 @@ export const WorkflowDiagramCanvasBase = ({
155163
const { rightDrawerState } = useRightDrawerState();
156164
const { isInRightDrawer } = useContext(ActionMenuContext);
157165

158-
const setWorkflowDiagram = useSetRecoilComponentStateV2(
159-
workflowDiagramComponentState,
160-
);
161-
162166
const handleEdgesChange = (
163167
edgeChanges: Array<EdgeChange<WorkflowDiagramEdge>>,
164168
) => {
@@ -188,97 +192,172 @@ export const WorkflowDiagramCanvasBase = ({
188192

189193
const containerRef = useRef<HTMLDivElement>(null);
190194

191-
const setFlowViewport = useCallback(
192-
({
193-
rightDrawerState,
194-
noAnimation,
195-
workflowDiagramFlowInitializationStatus,
196-
isInRightDrawer,
197-
}: {
198-
rightDrawerState: CommandMenuAnimationVariant;
199-
noAnimation?: boolean;
200-
workflowDiagramFlowInitializationStatus:
201-
| 'not-initialized'
202-
| 'initialized';
203-
isInRightDrawer: boolean;
204-
}) => {
205-
if (
206-
!isDefined(containerRef.current) ||
207-
workflowDiagramFlowInitializationStatus !== 'initialized'
208-
) {
209-
return;
210-
}
195+
const setFlowViewport = useRecoilCallback(
196+
() =>
197+
({
198+
rightDrawerState,
199+
noAnimation,
200+
workflowDiagramFlowInitialized,
201+
isInRightDrawer,
202+
workflowDiagram,
203+
}: {
204+
rightDrawerState: CommandMenuAnimationVariant;
205+
noAnimation?: boolean;
206+
workflowDiagramFlowInitialized: boolean;
207+
isInRightDrawer: boolean;
208+
workflowDiagram: WorkflowDiagram | undefined;
209+
}) => {
210+
if (
211+
!isDefined(containerRef.current) ||
212+
!workflowDiagramFlowInitialized
213+
) {
214+
return;
215+
}
211216

212-
const currentViewport = reactflow.getViewport();
213-
const flowBounds = reactflow.getNodesBounds(reactflow.getNodes());
217+
const currentViewport = reactflow.getViewport();
218+
const nodes = workflowDiagram?.nodes ?? [];
214219

215-
let visibleRightDrawerWidth = 0;
216-
if (rightDrawerState === 'normal' && !isInRightDrawer) {
217-
const rightDrawerWidth = Number(
218-
THEME_COMMON.rightDrawerWidth.replace('px', ''),
220+
const canComputeNodesBounds = nodes.every((node) =>
221+
isDefined(node.measured),
219222
);
220223

221-
visibleRightDrawerWidth = rightDrawerWidth;
222-
}
224+
if (!canComputeNodesBounds) {
225+
setWorkflowDiagramWaitingNodesDimensions(true);
226+
return;
227+
}
228+
229+
setWorkflowDiagramWaitingNodesDimensions(false);
230+
231+
let visibleRightDrawerWidth = 0;
232+
if (rightDrawerState === 'normal' && !isInRightDrawer) {
233+
const rightDrawerWidth = Number(
234+
THEME_COMMON.rightDrawerWidth.replace('px', ''),
235+
);
236+
237+
visibleRightDrawerWidth = rightDrawerWidth;
238+
}
223239

224-
const viewportX =
225-
(containerRef.current.offsetWidth + visibleRightDrawerWidth) / 2 -
226-
flowBounds.width / 2;
227-
228-
reactflow.setViewport(
229-
{
230-
...currentViewport,
231-
x: viewportX - visibleRightDrawerWidth,
232-
zoom: defaultFitViewOptions.maxZoom,
233-
},
234-
{ duration: noAnimation ? 0 : 300 },
235-
);
236-
},
237-
[reactflow],
240+
const flowBounds = reactflow.getNodesBounds(nodes);
241+
const viewportX =
242+
(containerRef.current.offsetWidth + visibleRightDrawerWidth) / 2 -
243+
flowBounds.width / 2;
244+
245+
reactflow.setViewport(
246+
{
247+
...currentViewport,
248+
x: viewportX - visibleRightDrawerWidth,
249+
zoom: defaultFitViewOptions.maxZoom,
250+
},
251+
{ duration: noAnimation ? 0 : 300 },
252+
);
253+
},
254+
[reactflow, setWorkflowDiagramWaitingNodesDimensions],
255+
);
256+
257+
const handleSetFlowViewportOnChange = useRecoilCallback(
258+
({ snapshot }) =>
259+
({
260+
rightDrawerState,
261+
workflowDiagramFlowInitialized,
262+
isInRightDrawer,
263+
}: {
264+
rightDrawerState: CommandMenuAnimationVariant;
265+
workflowDiagramFlowInitialized: boolean;
266+
isInRightDrawer: boolean;
267+
}) => {
268+
setFlowViewport({
269+
rightDrawerState,
270+
isInRightDrawer,
271+
workflowDiagramFlowInitialized,
272+
workflowDiagram: getSnapshotValue(snapshot, workflowDiagramState),
273+
});
274+
},
275+
[setFlowViewport, workflowDiagramState],
238276
);
239277

240278
useEffect(() => {
241-
setFlowViewport({
279+
handleSetFlowViewportOnChange({
242280
rightDrawerState,
281+
workflowDiagramFlowInitialized,
243282
isInRightDrawer,
244-
workflowDiagramFlowInitializationStatus,
245283
});
246284
}, [
285+
handleSetFlowViewportOnChange,
247286
isInRightDrawer,
248287
rightDrawerState,
249-
setFlowViewport,
250-
workflowDiagramFlowInitializationStatus,
288+
workflowDiagramFlowInitialized,
251289
]);
252290

253-
const handleNodesChanges = (changes: NodeChange<WorkflowDiagramNode>[]) => {
254-
setWorkflowDiagram((diagram) => {
255-
if (!isDefined(diagram)) {
256-
return diagram;
257-
}
291+
const handleNodesChanges = useRecoilCallback(
292+
({ snapshot, set }) =>
293+
(changes: NodeChange<WorkflowDiagramNode>[]) => {
294+
const workflowDiagram = getSnapshotValue(
295+
snapshot,
296+
workflowDiagramState,
297+
);
298+
let updatedWorkflowDiagram = workflowDiagram;
299+
if (isDefined(workflowDiagram)) {
300+
updatedWorkflowDiagram = {
301+
...workflowDiagram,
302+
nodes: applyNodeChanges(changes, workflowDiagram.nodes),
303+
};
304+
}
258305

259-
return {
260-
...diagram,
261-
nodes: applyNodeChanges(changes, diagram.nodes),
262-
};
263-
});
264-
};
306+
set(workflowDiagramState, updatedWorkflowDiagram);
265307

266-
const handleInit = () => {
267-
if (!isDefined(containerRef.current)) {
268-
return;
269-
}
308+
const workflowDiagramWaitingNodesDimensions = getSnapshotValue(
309+
snapshot,
310+
workflowDiagramWaitingNodesDimensionsState,
311+
);
312+
if (!workflowDiagramWaitingNodesDimensions) {
313+
return;
314+
}
270315

271-
setFlowViewport({
272-
rightDrawerState,
273-
noAnimation: true,
316+
setFlowViewport({
317+
rightDrawerState,
318+
noAnimation: true,
319+
isInRightDrawer,
320+
workflowDiagramFlowInitialized,
321+
workflowDiagram: updatedWorkflowDiagram,
322+
});
323+
},
324+
[
274325
isInRightDrawer,
275-
workflowDiagramFlowInitializationStatus: 'initialized',
276-
});
326+
rightDrawerState,
327+
setFlowViewport,
328+
workflowDiagramFlowInitialized,
329+
workflowDiagramState,
330+
workflowDiagramWaitingNodesDimensionsState,
331+
],
332+
);
333+
334+
const handleInit = useRecoilCallback(
335+
({ snapshot }) =>
336+
() => {
337+
if (!isDefined(containerRef.current)) {
338+
return;
339+
}
277340

278-
setWorkflowDiagramFlowInitializationStatus('initialized');
341+
setFlowViewport({
342+
rightDrawerState,
343+
noAnimation: true,
344+
isInRightDrawer,
345+
workflowDiagramFlowInitialized: true,
346+
workflowDiagram: getSnapshotValue(snapshot, workflowDiagramState),
347+
});
279348

280-
onInit?.();
281-
};
349+
setWorkflowDiagramFlowInitialized(true);
350+
351+
onInit?.();
352+
},
353+
[
354+
isInRightDrawer,
355+
onInit,
356+
rightDrawerState,
357+
setFlowViewport,
358+
workflowDiagramState,
359+
],
360+
);
282361

283362
return (
284363
<StyledResetReactflowStyles ref={containerRef}>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import { createComponentStateV2 } from '@/ui/utilities/state/component-state/utils/createComponentStateV2';
2+
import { WorkflowVisualizerComponentInstanceContext } from '@/workflow/workflow-diagram/states/contexts/WorkflowVisualizerComponentInstanceContext';
3+
4+
export const workflowDiagramWaitingNodesDimensionsComponentState =
5+
createComponentStateV2<boolean>({
6+
key: 'workflowDiagramWaitingNodesDimensionsComponentState',
7+
defaultValue: false,
8+
componentInstanceContext: WorkflowVisualizerComponentInstanceContext,
9+
});

0 commit comments

Comments
 (0)