Set viewport when nodes dimensions are ready (#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:
f9971a8fad/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
This commit is contained in:
committed by
GitHub
parent
58e1e69280
commit
79e5ccfd37
@ -1,18 +1,23 @@
|
|||||||
import { ActionMenuContext } from '@/action-menu/contexts/ActionMenuContext';
|
import { ActionMenuContext } from '@/action-menu/contexts/ActionMenuContext';
|
||||||
import { CommandMenuAnimationVariant } from '@/command-menu/types/CommandMenuAnimationVariant';
|
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 { getSnapshotValue } from '@/ui/utilities/recoil-scope/utils/getSnapshotValue';
|
||||||
|
import { useRecoilComponentCallbackStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackStateV2';
|
||||||
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';
|
||||||
import { WorkflowDiagramCustomMarkers } from '@/workflow/workflow-diagram/components/WorkflowDiagramCustomMarkers';
|
import { WorkflowDiagramCustomMarkers } from '@/workflow/workflow-diagram/components/WorkflowDiagramCustomMarkers';
|
||||||
import { useRightDrawerState } from '@/workflow/workflow-diagram/hooks/useRightDrawerState';
|
import { useRightDrawerState } from '@/workflow/workflow-diagram/hooks/useRightDrawerState';
|
||||||
import { workflowDiagramComponentState } from '@/workflow/workflow-diagram/states/workflowDiagramComponentState';
|
import { workflowDiagramComponentState } from '@/workflow/workflow-diagram/states/workflowDiagramComponentState';
|
||||||
|
import { workflowDiagramWaitingNodesDimensionsComponentState } from '@/workflow/workflow-diagram/states/workflowDiagramWaitingNodesDimensionsComponentState';
|
||||||
import {
|
import {
|
||||||
|
WorkflowDiagram,
|
||||||
WorkflowDiagramEdge,
|
WorkflowDiagramEdge,
|
||||||
WorkflowDiagramEdgeType,
|
WorkflowDiagramEdgeType,
|
||||||
WorkflowDiagramNode,
|
WorkflowDiagramNode,
|
||||||
WorkflowDiagramNodeType,
|
WorkflowDiagramNodeType,
|
||||||
} from '@/workflow/workflow-diagram/types/WorkflowDiagram';
|
} from '@/workflow/workflow-diagram/types/WorkflowDiagram';
|
||||||
import { getOrganizedDiagram } from '@/workflow/workflow-diagram/utils/getOrganizedDiagram';
|
import { getOrganizedDiagram } from '@/workflow/workflow-diagram/utils/getOrganizedDiagram';
|
||||||
|
import { workflowInsertStepIdsComponentState } from '@/workflow/workflow-steps/states/workflowInsertStepIdsComponentState';
|
||||||
import { useTheme } from '@emotion/react';
|
import { useTheme } from '@emotion/react';
|
||||||
import styled from '@emotion/styled';
|
import styled from '@emotion/styled';
|
||||||
import {
|
import {
|
||||||
@ -28,18 +33,11 @@ import {
|
|||||||
useReactFlow,
|
useReactFlow,
|
||||||
} from '@xyflow/react';
|
} from '@xyflow/react';
|
||||||
import '@xyflow/react/dist/style.css';
|
import '@xyflow/react/dist/style.css';
|
||||||
import React, {
|
import React, { useContext, useEffect, useMemo, useRef, useState } from 'react';
|
||||||
useCallback,
|
import { useRecoilCallback } from 'recoil';
|
||||||
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';
|
||||||
import { workflowInsertStepIdsComponentState } from '@/workflow/workflow-steps/states/workflowInsertStepIdsComponentState';
|
|
||||||
|
|
||||||
const StyledResetReactflowStyles = styled.div`
|
const StyledResetReactflowStyles = styled.div`
|
||||||
height: 100%;
|
height: 100%;
|
||||||
@ -134,15 +132,25 @@ export const WorkflowDiagramCanvasBase = ({
|
|||||||
const workflowDiagram = useRecoilComponentValueV2(
|
const workflowDiagram = useRecoilComponentValueV2(
|
||||||
workflowDiagramComponentState,
|
workflowDiagramComponentState,
|
||||||
);
|
);
|
||||||
|
const workflowDiagramState = useRecoilComponentCallbackStateV2(
|
||||||
|
workflowDiagramComponentState,
|
||||||
|
);
|
||||||
|
const setWorkflowDiagram = useSetRecoilComponentStateV2(
|
||||||
|
workflowDiagramComponentState,
|
||||||
|
);
|
||||||
const setWorkflowInsertStepIds = useSetRecoilComponentStateV2(
|
const setWorkflowInsertStepIds = useSetRecoilComponentStateV2(
|
||||||
workflowInsertStepIdsComponentState,
|
workflowInsertStepIdsComponentState,
|
||||||
);
|
);
|
||||||
|
const workflowDiagramWaitingNodesDimensionsState =
|
||||||
|
useRecoilComponentCallbackStateV2(
|
||||||
|
workflowDiagramWaitingNodesDimensionsComponentState,
|
||||||
|
);
|
||||||
|
const setWorkflowDiagramWaitingNodesDimensions = useSetRecoilComponentStateV2(
|
||||||
|
workflowDiagramWaitingNodesDimensionsComponentState,
|
||||||
|
);
|
||||||
|
|
||||||
const [
|
const [workflowDiagramFlowInitialized, setWorkflowDiagramFlowInitialized] =
|
||||||
workflowDiagramFlowInitializationStatus,
|
useState<boolean>(false);
|
||||||
setWorkflowDiagramFlowInitializationStatus,
|
|
||||||
] = useState<'not-initialized' | 'initialized'>('not-initialized');
|
|
||||||
|
|
||||||
const { nodes, edges } = useMemo(
|
const { nodes, edges } = useMemo(
|
||||||
() =>
|
() =>
|
||||||
@ -155,10 +163,6 @@ export const WorkflowDiagramCanvasBase = ({
|
|||||||
const { rightDrawerState } = useRightDrawerState();
|
const { rightDrawerState } = useRightDrawerState();
|
||||||
const { isInRightDrawer } = useContext(ActionMenuContext);
|
const { isInRightDrawer } = useContext(ActionMenuContext);
|
||||||
|
|
||||||
const setWorkflowDiagram = useSetRecoilComponentStateV2(
|
|
||||||
workflowDiagramComponentState,
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleEdgesChange = (
|
const handleEdgesChange = (
|
||||||
edgeChanges: Array<EdgeChange<WorkflowDiagramEdge>>,
|
edgeChanges: Array<EdgeChange<WorkflowDiagramEdge>>,
|
||||||
) => {
|
) => {
|
||||||
@ -188,97 +192,172 @@ export const WorkflowDiagramCanvasBase = ({
|
|||||||
|
|
||||||
const containerRef = useRef<HTMLDivElement>(null);
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
const setFlowViewport = useCallback(
|
const setFlowViewport = useRecoilCallback(
|
||||||
({
|
() =>
|
||||||
rightDrawerState,
|
({
|
||||||
noAnimation,
|
rightDrawerState,
|
||||||
workflowDiagramFlowInitializationStatus,
|
noAnimation,
|
||||||
isInRightDrawer,
|
workflowDiagramFlowInitialized,
|
||||||
}: {
|
isInRightDrawer,
|
||||||
rightDrawerState: CommandMenuAnimationVariant;
|
workflowDiagram,
|
||||||
noAnimation?: boolean;
|
}: {
|
||||||
workflowDiagramFlowInitializationStatus:
|
rightDrawerState: CommandMenuAnimationVariant;
|
||||||
| 'not-initialized'
|
noAnimation?: boolean;
|
||||||
| 'initialized';
|
workflowDiagramFlowInitialized: boolean;
|
||||||
isInRightDrawer: boolean;
|
isInRightDrawer: boolean;
|
||||||
}) => {
|
workflowDiagram: WorkflowDiagram | undefined;
|
||||||
if (
|
}) => {
|
||||||
!isDefined(containerRef.current) ||
|
if (
|
||||||
workflowDiagramFlowInitializationStatus !== 'initialized'
|
!isDefined(containerRef.current) ||
|
||||||
) {
|
!workflowDiagramFlowInitialized
|
||||||
return;
|
) {
|
||||||
}
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const currentViewport = reactflow.getViewport();
|
const currentViewport = reactflow.getViewport();
|
||||||
const flowBounds = reactflow.getNodesBounds(reactflow.getNodes());
|
const nodes = workflowDiagram?.nodes ?? [];
|
||||||
|
|
||||||
let visibleRightDrawerWidth = 0;
|
const canComputeNodesBounds = nodes.every((node) =>
|
||||||
if (rightDrawerState === 'normal' && !isInRightDrawer) {
|
isDefined(node.measured),
|
||||||
const rightDrawerWidth = Number(
|
|
||||||
THEME_COMMON.rightDrawerWidth.replace('px', ''),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
visibleRightDrawerWidth = rightDrawerWidth;
|
if (!canComputeNodesBounds) {
|
||||||
}
|
setWorkflowDiagramWaitingNodesDimensions(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const viewportX =
|
setWorkflowDiagramWaitingNodesDimensions(false);
|
||||||
(containerRef.current.offsetWidth + visibleRightDrawerWidth) / 2 -
|
|
||||||
flowBounds.width / 2;
|
|
||||||
|
|
||||||
reactflow.setViewport(
|
let visibleRightDrawerWidth = 0;
|
||||||
{
|
if (rightDrawerState === 'normal' && !isInRightDrawer) {
|
||||||
...currentViewport,
|
const rightDrawerWidth = Number(
|
||||||
x: viewportX - visibleRightDrawerWidth,
|
THEME_COMMON.rightDrawerWidth.replace('px', ''),
|
||||||
zoom: defaultFitViewOptions.maxZoom,
|
);
|
||||||
},
|
|
||||||
{ duration: noAnimation ? 0 : 300 },
|
visibleRightDrawerWidth = rightDrawerWidth;
|
||||||
);
|
}
|
||||||
},
|
|
||||||
[reactflow],
|
const flowBounds = reactflow.getNodesBounds(nodes);
|
||||||
|
const viewportX =
|
||||||
|
(containerRef.current.offsetWidth + visibleRightDrawerWidth) / 2 -
|
||||||
|
flowBounds.width / 2;
|
||||||
|
|
||||||
|
reactflow.setViewport(
|
||||||
|
{
|
||||||
|
...currentViewport,
|
||||||
|
x: viewportX - visibleRightDrawerWidth,
|
||||||
|
zoom: defaultFitViewOptions.maxZoom,
|
||||||
|
},
|
||||||
|
{ duration: noAnimation ? 0 : 300 },
|
||||||
|
);
|
||||||
|
},
|
||||||
|
[reactflow, setWorkflowDiagramWaitingNodesDimensions],
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleSetFlowViewportOnChange = useRecoilCallback(
|
||||||
|
({ snapshot }) =>
|
||||||
|
({
|
||||||
|
rightDrawerState,
|
||||||
|
workflowDiagramFlowInitialized,
|
||||||
|
isInRightDrawer,
|
||||||
|
}: {
|
||||||
|
rightDrawerState: CommandMenuAnimationVariant;
|
||||||
|
workflowDiagramFlowInitialized: boolean;
|
||||||
|
isInRightDrawer: boolean;
|
||||||
|
}) => {
|
||||||
|
setFlowViewport({
|
||||||
|
rightDrawerState,
|
||||||
|
isInRightDrawer,
|
||||||
|
workflowDiagramFlowInitialized,
|
||||||
|
workflowDiagram: getSnapshotValue(snapshot, workflowDiagramState),
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[setFlowViewport, workflowDiagramState],
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setFlowViewport({
|
handleSetFlowViewportOnChange({
|
||||||
rightDrawerState,
|
rightDrawerState,
|
||||||
|
workflowDiagramFlowInitialized,
|
||||||
isInRightDrawer,
|
isInRightDrawer,
|
||||||
workflowDiagramFlowInitializationStatus,
|
|
||||||
});
|
});
|
||||||
}, [
|
}, [
|
||||||
|
handleSetFlowViewportOnChange,
|
||||||
isInRightDrawer,
|
isInRightDrawer,
|
||||||
rightDrawerState,
|
rightDrawerState,
|
||||||
setFlowViewport,
|
workflowDiagramFlowInitialized,
|
||||||
workflowDiagramFlowInitializationStatus,
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const handleNodesChanges = (changes: NodeChange<WorkflowDiagramNode>[]) => {
|
const handleNodesChanges = useRecoilCallback(
|
||||||
setWorkflowDiagram((diagram) => {
|
({ snapshot, set }) =>
|
||||||
if (!isDefined(diagram)) {
|
(changes: NodeChange<WorkflowDiagramNode>[]) => {
|
||||||
return diagram;
|
const workflowDiagram = getSnapshotValue(
|
||||||
}
|
snapshot,
|
||||||
|
workflowDiagramState,
|
||||||
|
);
|
||||||
|
let updatedWorkflowDiagram = workflowDiagram;
|
||||||
|
if (isDefined(workflowDiagram)) {
|
||||||
|
updatedWorkflowDiagram = {
|
||||||
|
...workflowDiagram,
|
||||||
|
nodes: applyNodeChanges(changes, workflowDiagram.nodes),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
set(workflowDiagramState, updatedWorkflowDiagram);
|
||||||
...diagram,
|
|
||||||
nodes: applyNodeChanges(changes, diagram.nodes),
|
|
||||||
};
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleInit = () => {
|
const workflowDiagramWaitingNodesDimensions = getSnapshotValue(
|
||||||
if (!isDefined(containerRef.current)) {
|
snapshot,
|
||||||
return;
|
workflowDiagramWaitingNodesDimensionsState,
|
||||||
}
|
);
|
||||||
|
if (!workflowDiagramWaitingNodesDimensions) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
setFlowViewport({
|
setFlowViewport({
|
||||||
rightDrawerState,
|
rightDrawerState,
|
||||||
noAnimation: true,
|
noAnimation: true,
|
||||||
|
isInRightDrawer,
|
||||||
|
workflowDiagramFlowInitialized,
|
||||||
|
workflowDiagram: updatedWorkflowDiagram,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[
|
||||||
isInRightDrawer,
|
isInRightDrawer,
|
||||||
workflowDiagramFlowInitializationStatus: 'initialized',
|
rightDrawerState,
|
||||||
});
|
setFlowViewport,
|
||||||
|
workflowDiagramFlowInitialized,
|
||||||
|
workflowDiagramState,
|
||||||
|
workflowDiagramWaitingNodesDimensionsState,
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
setWorkflowDiagramFlowInitializationStatus('initialized');
|
const handleInit = useRecoilCallback(
|
||||||
|
({ snapshot }) =>
|
||||||
|
() => {
|
||||||
|
if (!isDefined(containerRef.current)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
onInit?.();
|
setFlowViewport({
|
||||||
};
|
rightDrawerState,
|
||||||
|
noAnimation: true,
|
||||||
|
isInRightDrawer,
|
||||||
|
workflowDiagramFlowInitialized: true,
|
||||||
|
workflowDiagram: getSnapshotValue(snapshot, workflowDiagramState),
|
||||||
|
});
|
||||||
|
|
||||||
|
setWorkflowDiagramFlowInitialized(true);
|
||||||
|
|
||||||
|
onInit?.();
|
||||||
|
},
|
||||||
|
[
|
||||||
|
isInRightDrawer,
|
||||||
|
onInit,
|
||||||
|
rightDrawerState,
|
||||||
|
setFlowViewport,
|
||||||
|
workflowDiagramState,
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<StyledResetReactflowStyles ref={containerRef}>
|
<StyledResetReactflowStyles ref={containerRef}>
|
||||||
|
|||||||
@ -0,0 +1,9 @@
|
|||||||
|
import { createComponentStateV2 } from '@/ui/utilities/state/component-state/utils/createComponentStateV2';
|
||||||
|
import { WorkflowVisualizerComponentInstanceContext } from '@/workflow/workflow-diagram/states/contexts/WorkflowVisualizerComponentInstanceContext';
|
||||||
|
|
||||||
|
export const workflowDiagramWaitingNodesDimensionsComponentState =
|
||||||
|
createComponentStateV2<boolean>({
|
||||||
|
key: 'workflowDiagramWaitingNodesDimensionsComponentState',
|
||||||
|
defaultValue: false,
|
||||||
|
componentInstanceContext: WorkflowVisualizerComponentInstanceContext,
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user