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