diff --git a/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowDiagramCanvasBase.tsx b/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowDiagramCanvasBase.tsx index 21c6cdc85..c8bf6f184 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowDiagramCanvasBase.tsx +++ b/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowDiagramCanvasBase.tsx @@ -1,18 +1,23 @@ import { ActionMenuContext } from '@/action-menu/contexts/ActionMenuContext'; import { CommandMenuAnimationVariant } from '@/command-menu/types/CommandMenuAnimationVariant'; 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 { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2'; import { WorkflowDiagramCustomMarkers } from '@/workflow/workflow-diagram/components/WorkflowDiagramCustomMarkers'; import { useRightDrawerState } from '@/workflow/workflow-diagram/hooks/useRightDrawerState'; import { workflowDiagramComponentState } from '@/workflow/workflow-diagram/states/workflowDiagramComponentState'; +import { workflowDiagramWaitingNodesDimensionsComponentState } from '@/workflow/workflow-diagram/states/workflowDiagramWaitingNodesDimensionsComponentState'; import { + WorkflowDiagram, WorkflowDiagramEdge, WorkflowDiagramEdgeType, WorkflowDiagramNode, WorkflowDiagramNodeType, } from '@/workflow/workflow-diagram/types/WorkflowDiagram'; import { getOrganizedDiagram } from '@/workflow/workflow-diagram/utils/getOrganizedDiagram'; +import { workflowInsertStepIdsComponentState } from '@/workflow/workflow-steps/states/workflowInsertStepIdsComponentState'; import { useTheme } from '@emotion/react'; import styled from '@emotion/styled'; import { @@ -28,18 +33,11 @@ import { useReactFlow, } from '@xyflow/react'; import '@xyflow/react/dist/style.css'; -import React, { - useCallback, - useContext, - useEffect, - useMemo, - useRef, - useState, -} from 'react'; +import React, { useContext, useEffect, useMemo, useRef, useState } from 'react'; +import { useRecoilCallback } from 'recoil'; import { isDefined } from 'twenty-shared/utils'; import { Tag, TagColor } from 'twenty-ui/components'; import { THEME_COMMON } from 'twenty-ui/theme'; -import { workflowInsertStepIdsComponentState } from '@/workflow/workflow-steps/states/workflowInsertStepIdsComponentState'; const StyledResetReactflowStyles = styled.div` height: 100%; @@ -134,15 +132,25 @@ export const WorkflowDiagramCanvasBase = ({ const workflowDiagram = useRecoilComponentValueV2( workflowDiagramComponentState, ); - + const workflowDiagramState = useRecoilComponentCallbackStateV2( + workflowDiagramComponentState, + ); + const setWorkflowDiagram = useSetRecoilComponentStateV2( + workflowDiagramComponentState, + ); const setWorkflowInsertStepIds = useSetRecoilComponentStateV2( workflowInsertStepIdsComponentState, ); + const workflowDiagramWaitingNodesDimensionsState = + useRecoilComponentCallbackStateV2( + workflowDiagramWaitingNodesDimensionsComponentState, + ); + const setWorkflowDiagramWaitingNodesDimensions = useSetRecoilComponentStateV2( + workflowDiagramWaitingNodesDimensionsComponentState, + ); - const [ - workflowDiagramFlowInitializationStatus, - setWorkflowDiagramFlowInitializationStatus, - ] = useState<'not-initialized' | 'initialized'>('not-initialized'); + const [workflowDiagramFlowInitialized, setWorkflowDiagramFlowInitialized] = + useState(false); const { nodes, edges } = useMemo( () => @@ -155,10 +163,6 @@ export const WorkflowDiagramCanvasBase = ({ const { rightDrawerState } = useRightDrawerState(); const { isInRightDrawer } = useContext(ActionMenuContext); - const setWorkflowDiagram = useSetRecoilComponentStateV2( - workflowDiagramComponentState, - ); - const handleEdgesChange = ( edgeChanges: Array>, ) => { @@ -188,97 +192,172 @@ export const WorkflowDiagramCanvasBase = ({ const containerRef = useRef(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 setFlowViewport = useRecoilCallback( + () => + ({ + rightDrawerState, + noAnimation, + workflowDiagramFlowInitialized, + isInRightDrawer, + workflowDiagram, + }: { + rightDrawerState: CommandMenuAnimationVariant; + noAnimation?: boolean; + workflowDiagramFlowInitialized: boolean; + isInRightDrawer: boolean; + workflowDiagram: WorkflowDiagram | undefined; + }) => { + if ( + !isDefined(containerRef.current) || + !workflowDiagramFlowInitialized + ) { + return; + } - const currentViewport = reactflow.getViewport(); - const flowBounds = reactflow.getNodesBounds(reactflow.getNodes()); + const currentViewport = reactflow.getViewport(); + const nodes = workflowDiagram?.nodes ?? []; - let visibleRightDrawerWidth = 0; - if (rightDrawerState === 'normal' && !isInRightDrawer) { - const rightDrawerWidth = Number( - THEME_COMMON.rightDrawerWidth.replace('px', ''), + const canComputeNodesBounds = nodes.every((node) => + isDefined(node.measured), ); - visibleRightDrawerWidth = rightDrawerWidth; - } + if (!canComputeNodesBounds) { + setWorkflowDiagramWaitingNodesDimensions(true); + return; + } - const viewportX = - (containerRef.current.offsetWidth + visibleRightDrawerWidth) / 2 - - flowBounds.width / 2; + setWorkflowDiagramWaitingNodesDimensions(false); - reactflow.setViewport( - { - ...currentViewport, - x: viewportX - visibleRightDrawerWidth, - zoom: defaultFitViewOptions.maxZoom, - }, - { duration: noAnimation ? 0 : 300 }, - ); - }, - [reactflow], + let visibleRightDrawerWidth = 0; + if (rightDrawerState === 'normal' && !isInRightDrawer) { + const rightDrawerWidth = Number( + THEME_COMMON.rightDrawerWidth.replace('px', ''), + ); + + visibleRightDrawerWidth = rightDrawerWidth; + } + + 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(() => { - setFlowViewport({ + handleSetFlowViewportOnChange({ rightDrawerState, + workflowDiagramFlowInitialized, isInRightDrawer, - workflowDiagramFlowInitializationStatus, }); }, [ + handleSetFlowViewportOnChange, isInRightDrawer, rightDrawerState, - setFlowViewport, - workflowDiagramFlowInitializationStatus, + workflowDiagramFlowInitialized, ]); - const handleNodesChanges = (changes: NodeChange[]) => { - setWorkflowDiagram((diagram) => { - if (!isDefined(diagram)) { - return diagram; - } + const handleNodesChanges = useRecoilCallback( + ({ snapshot, set }) => + (changes: NodeChange[]) => { + const workflowDiagram = getSnapshotValue( + snapshot, + workflowDiagramState, + ); + let updatedWorkflowDiagram = workflowDiagram; + if (isDefined(workflowDiagram)) { + updatedWorkflowDiagram = { + ...workflowDiagram, + nodes: applyNodeChanges(changes, workflowDiagram.nodes), + }; + } - return { - ...diagram, - nodes: applyNodeChanges(changes, diagram.nodes), - }; - }); - }; + set(workflowDiagramState, updatedWorkflowDiagram); - const handleInit = () => { - if (!isDefined(containerRef.current)) { - return; - } + const workflowDiagramWaitingNodesDimensions = getSnapshotValue( + snapshot, + workflowDiagramWaitingNodesDimensionsState, + ); + if (!workflowDiagramWaitingNodesDimensions) { + return; + } - setFlowViewport({ - rightDrawerState, - noAnimation: true, + setFlowViewport({ + rightDrawerState, + noAnimation: true, + isInRightDrawer, + workflowDiagramFlowInitialized, + workflowDiagram: updatedWorkflowDiagram, + }); + }, + [ 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 ( diff --git a/packages/twenty-front/src/modules/workflow/workflow-diagram/states/workflowDiagramWaitingNodesDimensionsComponentState.ts b/packages/twenty-front/src/modules/workflow/workflow-diagram/states/workflowDiagramWaitingNodesDimensionsComponentState.ts new file mode 100644 index 000000000..fe3cf932c --- /dev/null +++ b/packages/twenty-front/src/modules/workflow/workflow-diagram/states/workflowDiagramWaitingNodesDimensionsComponentState.ts @@ -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({ + key: 'workflowDiagramWaitingNodesDimensionsComponentState', + defaultValue: false, + componentInstanceContext: WorkflowVisualizerComponentInstanceContext, + });