Move the viewport of the workflow visualizer on the show page when side panel is opened (#12605)

The viewport was moved before the flow finished initializing. I created
a state to prevent moving the viewport before the flow has been
initialized, allowing us to compute the bounds of the nodes correctly.

## Before


https://github.com/user-attachments/assets/0f034daf-c29c-4d54-905b-191eb60477a9

## After



https://github.com/user-attachments/assets/1f9018ad-ff97-4cf2-997e-d6b7dadf1f30

## Bonus 🎉 

The viewport is no longer progressively zoomed out, as it was before:



https://github.com/user-attachments/assets/0b985c22-ef06-4226-92a0-e5da569876ff
This commit is contained in:
Baptiste Devessier
2025-06-16 09:58:31 +02:00
committed by GitHub
parent 94376e8509
commit 43bc55efcd

View File

@ -1,3 +1,5 @@
import { ActionMenuContext } from '@/action-menu/contexts/ActionMenuContext';
import { CommandMenuAnimationVariant } from '@/command-menu/types/CommandMenuAnimationVariant';
import { useListenRightDrawerClose } from '@/ui/layout/right-drawer/hooks/useListenRightDrawerClose';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2';
@ -26,7 +28,14 @@ import {
useReactFlow,
} from '@xyflow/react';
import '@xyflow/react/dist/style.css';
import React, { useEffect, useMemo, useRef } from 'react';
import React, {
useCallback,
useContext,
useEffect,
useMemo,
useRef,
useState,
} from 'react';
import { isDefined } from 'twenty-shared/utils';
import { Tag, TagColor } from 'twenty-ui/components';
import { THEME_COMMON } from 'twenty-ui/theme';
@ -124,6 +133,10 @@ export const WorkflowDiagramCanvasBase = ({
const workflowDiagram = useRecoilComponentValueV2(
workflowDiagramComponentState,
);
const [
workflowDiagramFlowInitializationStatus,
setWorkflowDiagramFlowInitializationStatus,
] = useState<'not-initialized' | 'initialized'>('not-initialized');
const { nodes, edges } = useMemo(
() =>
@ -134,10 +147,7 @@ export const WorkflowDiagramCanvasBase = ({
);
const { rightDrawerState } = useRightDrawerState();
const rightDrawerWidth = Number(
THEME_COMMON.rightDrawerWidth.replace('px', ''),
);
const { isInRightDrawer } = useContext(ActionMenuContext);
const setWorkflowDiagram = useSetRecoilComponentStateV2(
workflowDiagramComponentState,
@ -168,32 +178,67 @@ export const WorkflowDiagramCanvasBase = ({
const containerRef = useRef<HTMLDivElement>(null);
const setFlowViewport = useCallback(
({
rightDrawerState,
noAnimation,
workflowDiagramFlowInitializationStatus,
isInRightDrawer,
}: {
rightDrawerState: CommandMenuAnimationVariant;
noAnimation?: boolean;
workflowDiagramFlowInitializationStatus:
| 'not-initialized'
| 'initialized';
isInRightDrawer: boolean;
}) => {
if (
!isDefined(containerRef.current) ||
workflowDiagramFlowInitializationStatus !== 'initialized'
) {
return;
}
const currentViewport = reactflow.getViewport();
const flowBounds = reactflow.getNodesBounds(reactflow.getNodes());
let visibleRightDrawerWidth = 0;
if (rightDrawerState === 'normal' && !isInRightDrawer) {
const rightDrawerWidth = Number(
THEME_COMMON.rightDrawerWidth.replace('px', ''),
);
visibleRightDrawerWidth = rightDrawerWidth;
}
const viewportX =
(containerRef.current.offsetWidth + visibleRightDrawerWidth) / 2 -
flowBounds.width / 2;
reactflow.setViewport(
{
...currentViewport,
x: viewportX - visibleRightDrawerWidth,
zoom: defaultFitViewOptions.maxZoom,
},
{ duration: noAnimation ? 0 : 300 },
);
},
[reactflow],
);
useEffect(() => {
if (!isDefined(containerRef.current) || !reactflow.viewportInitialized) {
return;
}
const currentViewport = reactflow.getViewport();
const flowBounds = reactflow.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]);
setFlowViewport({
rightDrawerState,
isInRightDrawer,
workflowDiagramFlowInitializationStatus,
});
}, [
isInRightDrawer,
rightDrawerState,
setFlowViewport,
workflowDiagramFlowInitializationStatus,
]);
const handleNodesChanges = (changes: NodeChange<WorkflowDiagramNode>[]) => {
setWorkflowDiagram((diagram) => {
@ -213,14 +258,15 @@ export const WorkflowDiagramCanvasBase = ({
return;
}
const flowBounds = reactflow.getNodesBounds(reactflow.getNodes());
reactflow.setViewport({
x: containerRef.current.offsetWidth / 2 - flowBounds.width / 2,
y: 150,
zoom: defaultFitViewOptions.maxZoom,
setFlowViewport({
rightDrawerState,
noAnimation: true,
isInRightDrawer,
workflowDiagramFlowInitializationStatus: 'initialized',
});
setWorkflowDiagramFlowInitializationStatus('initialized');
onInit?.();
};
@ -232,6 +278,7 @@ export const WorkflowDiagramCanvasBase = ({
onInit={handleInit}
minZoom={defaultFitViewOptions.minZoom}
maxZoom={defaultFitViewOptions.maxZoom}
defaultViewport={{ x: 0, y: 150, zoom: defaultFitViewOptions.maxZoom }}
nodeTypes={nodeTypes}
edgeTypes={edgeTypes}
nodes={nodes}