From b8cd4491c2e2390f0956b83543a6eb58b93cbc4e Mon Sep 17 00:00:00 2001 From: Baptiste Devessier Date: Wed, 6 Aug 2025 19:00:48 +0200 Subject: [PATCH] Replace `useOnSelectionChange` with `onClick` listeners for steps opening (#13672) This PR cleans a few parts of the workflow features and fixes a few improper behaviors. Here is a non-exaustive list of improved things: - Clicking on an action will always reset the command menu's navigation stack. Previously, the behavior wasn't unified between workflows, workflow versions and workflow runs. - Opening a step in the side panel is now down in the `onClick` event handler put on individual Reactflow nodes. This makes interoperability between filters (which are buttons on edges) and traditional actions. - Simplified the code that automatically opens pending forms in the side panel. This feature should now work more predictably. - Splitted the `WorkflowDiagramEmptyTrigger` component into `WorkflowDiagramEmptyTriggerEditable` and `WorkflowDiagramEmptyTriggerReadonly`. This makes handling events easier as the behavior isn't the same in both cases. - Dropped all `useOnSelectionChange` hooks that were used to open steps in the side panel upon user selection. - Created a specific `WorkflowRunDiagramStepNode` instead of using `WorkflowDiagramStepNodeReadonly` in `WorkflowRunDiagramCanvas` - Deleted `useHandleWorkflowRunDiagramCanvasInit` as it was used to open the initially selected step in the command menu, which is now handled in an effect that works for all cases, including workflow run's state refreshing. Closes https://github.com/twentyhq/core-team-issues/issues/1227 Closes https://github.com/twentyhq/twenty/issues/11923 --- ...CommandMenuWorkflowSelectActionContent.tsx | 37 +++++- ...kflowRunOpeningInCommandMenuSideEffects.ts | 8 +- .../WorkflowDiagramCanvasEditable.tsx | 6 +- .../WorkflowDiagramCanvasEditableEffect.tsx | 104 +-------------- .../WorkflowDiagramCanvasReadonly.tsx | 7 +- .../WorkflowDiagramCanvasReadonlyEffect.tsx | 76 ----------- .../WorkflowDiagramDefaultEdgeEditable.tsx | 20 ++- .../WorkflowDiagramEmptyTrigger.tsx | 23 ---- .../WorkflowDiagramEmptyTriggerEditable.tsx | 57 ++++++++ .../WorkflowDiagramEmptyTriggerReadonly.tsx | 82 ++++++++++++ .../WorkflowDiagramFilterEdgeEditable.tsx | 19 ++- .../WorkflowDiagramFilterEdgeReadonly.tsx | 14 ++ .../WorkflowDiagramStepNodeBase.tsx | 3 + .../WorkflowDiagramStepNodeEditable.tsx | 41 ++++++ ...WorkflowDiagramStepNodeEditableContent.tsx | 13 +- .../WorkflowDiagramStepNodeReadonly.tsx | 56 +++++++- .../components/WorkflowRunDiagramCanvas.tsx | 12 +- .../WorkflowRunDiagramCanvasEffect.tsx | 91 ------------- .../components/WorkflowRunDiagramStepNode.tsx | 75 +++++++++++ .../WorkflowRunVisualizerEffect.tsx | 124 +++++++++--------- ...owDiagramEmptyTriggerEditable.stories.tsx} | 20 +-- .../constants/CreateStepNodeWidth.ts | 8 -- .../useTriggerNodeSelection.test.tsx | 79 ----------- .../useHandleWorkflowRunDiagramCanvasInit.ts | 124 ------------------ .../hooks/useStartNodeCreation.ts | 23 +++- .../hooks/useTriggerNodeSelection.ts | 39 ------ ...agramTriggerNodeSelectionComponentState.ts | 9 -- ...mAutomaticallyOpenedStepsComponentState.ts | 2 +- ...lowRunStepToOpenByDefaultComponentState.ts | 16 --- .../workflow-steps/hooks/useDeleteStep.ts | 5 +- .../hooks/useVariableDropdown.ts | 51 ++++++- 31 files changed, 564 insertions(+), 680 deletions(-) delete mode 100644 packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowDiagramCanvasReadonlyEffect.tsx delete mode 100644 packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowDiagramEmptyTrigger.tsx create mode 100644 packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowDiagramEmptyTriggerEditable.tsx create mode 100644 packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowDiagramEmptyTriggerReadonly.tsx delete mode 100644 packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowRunDiagramCanvasEffect.tsx create mode 100644 packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowRunDiagramStepNode.tsx rename packages/twenty-front/src/modules/workflow/workflow-diagram/components/__stories__/{WorkflowDiagramEmptyTrigger.stories.tsx => WorkflowDiagramEmptyTriggerEditable.stories.tsx} (76%) delete mode 100644 packages/twenty-front/src/modules/workflow/workflow-diagram/constants/CreateStepNodeWidth.ts delete mode 100644 packages/twenty-front/src/modules/workflow/workflow-diagram/hooks/__tests__/useTriggerNodeSelection.test.tsx delete mode 100644 packages/twenty-front/src/modules/workflow/workflow-diagram/hooks/useHandleWorkflowRunDiagramCanvasInit.ts delete mode 100644 packages/twenty-front/src/modules/workflow/workflow-diagram/hooks/useTriggerNodeSelection.ts delete mode 100644 packages/twenty-front/src/modules/workflow/workflow-diagram/states/workflowDiagramTriggerNodeSelectionComponentState.ts delete mode 100644 packages/twenty-front/src/modules/workflow/workflow-diagram/states/workflowRunStepToOpenByDefaultComponentState.ts diff --git a/packages/twenty-front/src/modules/command-menu/pages/workflow/action/components/CommandMenuWorkflowSelectActionContent.tsx b/packages/twenty-front/src/modules/command-menu/pages/workflow/action/components/CommandMenuWorkflowSelectActionContent.tsx index 12cc5e7b34..7cafc819fb 100644 --- a/packages/twenty-front/src/modules/command-menu/pages/workflow/action/components/CommandMenuWorkflowSelectActionContent.tsx +++ b/packages/twenty-front/src/modules/command-menu/pages/workflow/action/components/CommandMenuWorkflowSelectActionContent.tsx @@ -1,4 +1,8 @@ +import { useWorkflowCommandMenu } from '@/command-menu/hooks/useWorkflowCommandMenu'; +import { commandMenuNavigationStackState } from '@/command-menu/states/commandMenuNavigationStackState'; import { useRecoilComponentState } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentState'; +import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue'; +import { workflowVisualizerWorkflowIdComponentState } from '@/workflow/states/workflowVisualizerWorkflowIdComponentState'; import { WorkflowActionType, WorkflowWithCurrentVersion, @@ -10,6 +14,9 @@ import { useCreateStep } from '@/workflow/workflow-steps/hooks/useCreateStep'; import { workflowInsertStepIdsComponentState } from '@/workflow/workflow-steps/states/workflowInsertStepIdsComponentState'; import { RECORD_ACTIONS } from '@/workflow/workflow-steps/workflow-actions/constants/RecordActions'; import { useFilteredOtherActions } from '@/workflow/workflow-steps/workflow-actions/hooks/useFilteredOtherActions'; +import { getActionIcon } from '@/workflow/workflow-steps/workflow-actions/utils/getActionIcon'; +import { useSetRecoilState } from 'recoil'; +import { isDefined } from 'twenty-shared/utils'; import { useIcons } from 'twenty-ui/display'; import { MenuItemCommand } from 'twenty-ui/navigation'; @@ -30,22 +37,50 @@ export const CommandMenuWorkflowSelectActionContent = ({ const [workflowInsertStepIds, setWorkflowInsertStepIds] = useRecoilComponentState(workflowInsertStepIdsComponentState); + const setCommandMenuNavigationStack = useSetRecoilState( + commandMenuNavigationStackState, + ); + + const workflowVisualizerWorkflowId = useRecoilComponentValue( + workflowVisualizerWorkflowIdComponentState, + ); + const { openWorkflowEditStepInCommandMenu } = useWorkflowCommandMenu(); + const handleCreateStep = async (actionType: WorkflowActionType) => { + if (!isDefined(workflowVisualizerWorkflowId)) { + throw new Error( + 'Workflow ID must be configured for the edge when creating a step', + ); + } + const { parentStepId, nextStepId, position } = workflowInsertStepIds; - await createStep({ + const createdStep = await createStep({ newStepType: actionType, parentStepId, nextStepId, position, }); + if (!isDefined(createdStep)) { + return; + } + setWorkflowInsertStepIds({ parentStepId: undefined, nextStepId: undefined, position: undefined, }); + closeRightClickMenu(); + + setCommandMenuNavigationStack([]); + + openWorkflowEditStepInCommandMenu( + workflowVisualizerWorkflowId, + createdStep.name, + getIcon(getActionIcon(createdStep.type as WorkflowActionType)), + ); }; return ( diff --git a/packages/twenty-front/src/modules/workflow/hooks/useRunWorkflowRunOpeningInCommandMenuSideEffects.ts b/packages/twenty-front/src/modules/workflow/hooks/useRunWorkflowRunOpeningInCommandMenuSideEffects.ts index a9f3af0dfb..3c7fe1d723 100644 --- a/packages/twenty-front/src/modules/workflow/hooks/useRunWorkflowRunOpeningInCommandMenuSideEffects.ts +++ b/packages/twenty-front/src/modules/workflow/hooks/useRunWorkflowRunOpeningInCommandMenuSideEffects.ts @@ -112,7 +112,13 @@ export const useRunWorkflowRunOpeningInCommandMenuSideEffects = () => { recordId, }), }), - (steps) => [...steps, stepToOpenByDefault.id], + (steps) => [ + ...steps, + { + stepId: stepToOpenByDefault.id, + isInRightDrawer: true, + }, + ], ); openWorkflowRunViewStepInCommandMenu({ workflowId: workflowRunRecord.workflowId, diff --git a/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowDiagramCanvasEditable.tsx b/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowDiagramCanvasEditable.tsx index 1438d9a341..e3975b0395 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowDiagramCanvasEditable.tsx +++ b/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowDiagramCanvasEditable.tsx @@ -4,7 +4,7 @@ import { WorkflowDiagramBlankEdge } from '@/workflow/workflow-diagram/components import { WorkflowDiagramCanvasBase } from '@/workflow/workflow-diagram/components/WorkflowDiagramCanvasBase'; import { WorkflowDiagramCanvasEditableEffect } from '@/workflow/workflow-diagram/components/WorkflowDiagramCanvasEditableEffect'; import { WorkflowDiagramDefaultEdgeEditable } from '@/workflow/workflow-diagram/components/WorkflowDiagramDefaultEdgeEditable'; -import { WorkflowDiagramEmptyTrigger } from '@/workflow/workflow-diagram/components/WorkflowDiagramEmptyTrigger'; +import { WorkflowDiagramEmptyTriggerEditable } from '@/workflow/workflow-diagram/components/WorkflowDiagramEmptyTriggerEditable'; import { WorkflowDiagramFilterEdgeEditable } from '@/workflow/workflow-diagram/components/WorkflowDiagramFilterEdgeEditable'; import { WorkflowDiagramFilteringDisabledEdgeEditable } from '@/workflow/workflow-diagram/components/WorkflowDiagramFilteringDisabledEdgeEditable'; import { WorkflowDiagramStepNodeEditable } from '@/workflow/workflow-diagram/components/WorkflowDiagramStepNodeEditable'; @@ -19,7 +19,7 @@ import { useCreateEdge } from '@/workflow/workflow-steps/hooks/useCreateEdge'; import { useDeleteEdge } from '@/workflow/workflow-steps/hooks/useDeleteEdge'; import { useUpdateStep } from '@/workflow/workflow-steps/hooks/useUpdateStep'; import { useUpdateWorkflowVersionTrigger } from '@/workflow/workflow-trigger/hooks/useUpdateWorkflowVersionTrigger'; -import { Connection, ReactFlowProvider, addEdge } from '@xyflow/react'; +import { addEdge, Connection, ReactFlowProvider } from '@xyflow/react'; import React from 'react'; import { isDefined } from 'twenty-shared/utils'; @@ -118,7 +118,7 @@ export const WorkflowDiagramCanvasEditable = ({ { - const { getIcon } = useIcons(); - - const { - openWorkflowTriggerTypeInCommandMenu, - openWorkflowEditStepInCommandMenu, - } = useWorkflowCommandMenu(); - const { setEdgeSelected, clearEdgeSelection } = useEdgeSelected(); const isWorkflowBranchEnabled = useIsFeatureEnabled( FeatureFlagKey.IS_WORKFLOW_BRANCH_ENABLED, ); - const setWorkflowSelectedNode = useSetRecoilComponentState( - workflowSelectedNodeComponentState, - ); - - const setCommandMenuNavigationStack = useSetRecoilState( - commandMenuNavigationStackState, - ); - - const { isInRightDrawer } = useContext(ActionMenuContext); - - const workflowVisualizerWorkflowId = useRecoilComponentValue( - workflowVisualizerWorkflowIdComponentState, - ); - - const handleSelectedNodes = useCallback( - ({ nodes }: OnSelectionChangeParams) => { - const selectedNode = nodes[0] as WorkflowDiagramNode | undefined; - - if (!isInRightDrawer) { - setCommandMenuNavigationStack([]); - } - - if (!isDefined(selectedNode)) { - return; - } - - const isEmptyTriggerNode = selectedNode.type === EMPTY_TRIGGER_STEP_ID; - if (isEmptyTriggerNode) { - if (isDefined(workflowVisualizerWorkflowId)) { - openWorkflowTriggerTypeInCommandMenu(workflowVisualizerWorkflowId); - return; - } - - return; - } - - const selectedNodeData = selectedNode.data as WorkflowDiagramStepNodeData; - - setWorkflowSelectedNode(selectedNode.id); - - if (isDefined(workflowVisualizerWorkflowId)) { - openWorkflowEditStepInCommandMenu( - workflowVisualizerWorkflowId, - selectedNodeData.name, - getIcon(getWorkflowNodeIconKey(selectedNodeData)), - ); - - return; - } - }, - [ - isInRightDrawer, - setCommandMenuNavigationStack, - workflowVisualizerWorkflowId, - openWorkflowTriggerTypeInCommandMenu, - openWorkflowEditStepInCommandMenu, - getIcon, - setWorkflowSelectedNode, - ], - ); - const handleSelectedEdges = useCallback( ({ edges }: OnSelectionChangeParams) => { if (!isWorkflowBranchEnabled) { @@ -119,19 +33,9 @@ export const WorkflowDiagramCanvasEditableEffect = () => { [isWorkflowBranchEnabled, setEdgeSelected, clearEdgeSelection], ); - const handleSelectionChange = useCallback( - (onSelectionChangeParams: OnSelectionChangeParams) => { - handleSelectedNodes(onSelectionChangeParams); - handleSelectedEdges(onSelectionChangeParams); - }, - [handleSelectedNodes, handleSelectedEdges], - ); - useOnSelectionChange({ - onChange: handleSelectionChange, + onChange: handleSelectedEdges, }); - useTriggerNodeSelection(); - return null; }; diff --git a/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowDiagramCanvasReadonly.tsx b/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowDiagramCanvasReadonly.tsx index e103e640b4..1eede1062c 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowDiagramCanvasReadonly.tsx +++ b/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowDiagramCanvasReadonly.tsx @@ -1,8 +1,7 @@ import { WorkflowVersionStatus } from '@/workflow/types/Workflow'; import { WorkflowDiagramCanvasBase } from '@/workflow/workflow-diagram/components/WorkflowDiagramCanvasBase'; -import { WorkflowDiagramCanvasReadonlyEffect } from '@/workflow/workflow-diagram/components/WorkflowDiagramCanvasReadonlyEffect'; import { WorkflowDiagramDefaultEdgeReadonly } from '@/workflow/workflow-diagram/components/WorkflowDiagramDefaultEdgeReadonly'; -import { WorkflowDiagramEmptyTrigger } from '@/workflow/workflow-diagram/components/WorkflowDiagramEmptyTrigger'; +import { WorkflowDiagramEmptyTriggerReadonly } from '@/workflow/workflow-diagram/components/WorkflowDiagramEmptyTriggerReadonly'; import { WorkflowDiagramFilterEdgeReadonly } from '@/workflow/workflow-diagram/components/WorkflowDiagramFilterEdgeReadonly'; import { WorkflowDiagramFilteringDisabledEdgeReadonly } from '@/workflow/workflow-diagram/components/WorkflowDiagramFilteringDisabledEdgeReadonly'; import { WorkflowDiagramStepNodeReadonly } from '@/workflow/workflow-diagram/components/WorkflowDiagramStepNodeReadonly'; @@ -23,7 +22,7 @@ export const WorkflowDiagramCanvasReadonly = ({ - - ); }; diff --git a/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowDiagramCanvasReadonlyEffect.tsx b/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowDiagramCanvasReadonlyEffect.tsx deleted file mode 100644 index 32a3496a5b..0000000000 --- a/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowDiagramCanvasReadonlyEffect.tsx +++ /dev/null @@ -1,76 +0,0 @@ -import { useWorkflowCommandMenu } from '@/command-menu/hooks/useWorkflowCommandMenu'; -import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue'; -import { useSetRecoilComponentState } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentState'; -import { workflowVisualizerWorkflowIdComponentState } from '@/workflow/states/workflowVisualizerWorkflowIdComponentState'; -import { workflowVisualizerWorkflowVersionIdComponentState } from '@/workflow/states/workflowVisualizerWorkflowVersionIdComponentState'; -import { useTriggerNodeSelection } from '@/workflow/workflow-diagram/hooks/useTriggerNodeSelection'; -import { workflowSelectedNodeComponentState } from '@/workflow/workflow-diagram/states/workflowSelectedNodeComponentState'; -import { - WorkflowDiagramNode, - WorkflowDiagramStepNodeData, -} from '@/workflow/workflow-diagram/types/WorkflowDiagram'; -import { getWorkflowNodeIconKey } from '@/workflow/workflow-diagram/utils/getWorkflowNodeIconKey'; -import { OnSelectionChangeParams, useOnSelectionChange } from '@xyflow/react'; -import { useCallback } from 'react'; -import { isDefined } from 'twenty-shared/utils'; -import { useIcons } from 'twenty-ui/display'; - -export const WorkflowDiagramCanvasReadonlyEffect = () => { - const { getIcon } = useIcons(); - const setWorkflowSelectedNode = useSetRecoilComponentState( - workflowSelectedNodeComponentState, - ); - const { openWorkflowViewStepInCommandMenu } = useWorkflowCommandMenu(); - - const workflowVisualizerWorkflowId = useRecoilComponentValue( - workflowVisualizerWorkflowIdComponentState, - ); - const workflowVisualizerWorkflowVersionId = useRecoilComponentValue( - workflowVisualizerWorkflowVersionIdComponentState, - ); - - const handleSelectionChange = useCallback( - ({ nodes }: OnSelectionChangeParams) => { - if ( - !( - isDefined(workflowVisualizerWorkflowId) && - isDefined(workflowVisualizerWorkflowVersionId) - ) - ) { - return; - } - - const selectedNode = nodes[0] as WorkflowDiagramNode | undefined; - - if (!isDefined(selectedNode)) { - return; - } - - setWorkflowSelectedNode(selectedNode.id); - - const selectedNodeData = selectedNode.data as WorkflowDiagramStepNodeData; - - openWorkflowViewStepInCommandMenu({ - workflowId: workflowVisualizerWorkflowId, - workflowVersionId: workflowVisualizerWorkflowVersionId, - title: selectedNodeData.name, - icon: getIcon(getWorkflowNodeIconKey(selectedNodeData)), - }); - }, - [ - setWorkflowSelectedNode, - openWorkflowViewStepInCommandMenu, - workflowVisualizerWorkflowId, - workflowVisualizerWorkflowVersionId, - getIcon, - ], - ); - - useOnSelectionChange({ - onChange: handleSelectionChange, - }); - - useTriggerNodeSelection(); - - return null; -}; diff --git a/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowDiagramDefaultEdgeEditable.tsx b/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowDiagramDefaultEdgeEditable.tsx index 1a51318de0..6e49213c09 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowDiagramDefaultEdgeEditable.tsx +++ b/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowDiagramDefaultEdgeEditable.tsx @@ -1,19 +1,23 @@ +import { ActionMenuContext } from '@/action-menu/contexts/ActionMenuContext'; +import { commandMenuNavigationStackState } from '@/command-menu/states/commandMenuNavigationStackState'; import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue'; import { useWorkflowWithCurrentVersion } from '@/workflow/hooks/useWorkflowWithCurrentVersion'; import { workflowVisualizerWorkflowIdComponentState } from '@/workflow/states/workflowVisualizerWorkflowIdComponentState'; +import { WorkflowDiagramBaseEdge } from '@/workflow/workflow-diagram/components/WorkflowDiagramBaseEdge'; +import { WorkflowDiagramEdgeButtonGroup } from '@/workflow/workflow-diagram/components/WorkflowDiagramEdgeButtonGroup'; import { WorkflowDiagramEdgeV2Container } from '@/workflow/workflow-diagram/components/WorkflowDiagramEdgeV2Container'; import { WorkflowDiagramEdgeV2VisibilityContainer } from '@/workflow/workflow-diagram/components/WorkflowDiagramEdgeV2VisibilityContainer'; import { WORKFLOW_DIAGRAM_EDGE_OPTIONS_CLICK_OUTSIDE_ID } from '@/workflow/workflow-diagram/constants/WorkflowDiagramEdgeOptionsClickOutsideId'; +import { useEdgeHovered } from '@/workflow/workflow-diagram/hooks/useEdgeHovered'; import { useOpenWorkflowEditFilterInCommandMenu } from '@/workflow/workflow-diagram/hooks/useOpenWorkflowEditFilterInCommandMenu'; import { useStartNodeCreation } from '@/workflow/workflow-diagram/hooks/useStartNodeCreation'; import { WorkflowDiagramEdge } from '@/workflow/workflow-diagram/types/WorkflowDiagram'; import { useCreateStep } from '@/workflow/workflow-steps/hooks/useCreateStep'; -import { useEdgeHovered } from '@/workflow/workflow-diagram/hooks/useEdgeHovered'; import { EdgeLabelRenderer, EdgeProps, getBezierPath } from '@xyflow/react'; +import { useContext } from 'react'; +import { useSetRecoilState } from 'recoil'; import { isDefined } from 'twenty-shared/utils'; import { IconFilter, IconPlus } from 'twenty-ui/display'; -import { WorkflowDiagramBaseEdge } from '@/workflow/workflow-diagram/components/WorkflowDiagramBaseEdge'; -import { WorkflowDiagramEdgeButtonGroup } from '@/workflow/workflow-diagram/components/WorkflowDiagramEdgeButtonGroup'; type WorkflowDiagramDefaultEdgeEditableProps = EdgeProps; @@ -28,6 +32,8 @@ export const WorkflowDiagramDefaultEdgeEditable = ({ markerStart, markerEnd, }: WorkflowDiagramDefaultEdgeEditableProps) => { + const { isInRightDrawer } = useContext(ActionMenuContext); + const { isEdgeHovered } = useEdgeHovered(); const [edgePath, labelX, labelY] = getBezierPath({ @@ -51,6 +57,10 @@ export const WorkflowDiagramDefaultEdgeEditable = ({ nextStepId: target, }); + const setCommandMenuNavigationStack = useSetRecoilState( + commandMenuNavigationStackState, + ); + const { openWorkflowEditFilterInCommandMenu } = useOpenWorkflowEditFilterInCommandMenu(); @@ -65,6 +75,10 @@ export const WorkflowDiagramDefaultEdgeEditable = ({ return; } + if (!isInRightDrawer) { + setCommandMenuNavigationStack([]); + } + openWorkflowEditFilterInCommandMenu({ stepId: createdStep.id, stepName: createdStep.name, diff --git a/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowDiagramEmptyTrigger.tsx b/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowDiagramEmptyTrigger.tsx deleted file mode 100644 index 1dd4710f49..0000000000 --- a/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowDiagramEmptyTrigger.tsx +++ /dev/null @@ -1,23 +0,0 @@ -import { WorkflowDiagramStepNodeBase } from '@/workflow/workflow-diagram/components/WorkflowDiagramStepNodeBase'; -import styled from '@emotion/styled'; - -const StyledStepNodeLabelIconContainer = styled.div` - align-items: center; - background: ${({ theme }) => theme.background.transparent.light}; - border-radius: ${({ theme }) => theme.spacing(1)}; - display: flex; - justify-content: center; - padding: ${({ theme }) => theme.spacing(3)}; -`; - -export const WorkflowDiagramEmptyTrigger = () => { - return ( - } - /> - ); -}; diff --git a/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowDiagramEmptyTriggerEditable.tsx b/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowDiagramEmptyTriggerEditable.tsx new file mode 100644 index 0000000000..b3f2fd5da7 --- /dev/null +++ b/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowDiagramEmptyTriggerEditable.tsx @@ -0,0 +1,57 @@ +import { ActionMenuContext } from '@/action-menu/contexts/ActionMenuContext'; +import { useWorkflowCommandMenu } from '@/command-menu/hooks/useWorkflowCommandMenu'; +import { commandMenuNavigationStackState } from '@/command-menu/states/commandMenuNavigationStackState'; +import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue'; +import { workflowVisualizerWorkflowIdComponentState } from '@/workflow/states/workflowVisualizerWorkflowIdComponentState'; +import { WorkflowDiagramStepNodeBase } from '@/workflow/workflow-diagram/components/WorkflowDiagramStepNodeBase'; +import styled from '@emotion/styled'; +import { useLingui } from '@lingui/react/macro'; +import { useContext } from 'react'; +import { useSetRecoilState } from 'recoil'; +import { isDefined } from 'twenty-shared/utils'; + +const StyledStepNodeLabelIconContainer = styled.div` + align-items: center; + background: ${({ theme }) => theme.background.transparent.light}; + border-radius: ${({ theme }) => theme.spacing(1)}; + display: flex; + justify-content: center; + padding: ${({ theme }) => theme.spacing(3)}; +`; + +export const WorkflowDiagramEmptyTriggerEditable = () => { + const { t } = useLingui(); + + const { openWorkflowTriggerTypeInCommandMenu } = useWorkflowCommandMenu(); + + const workflowVisualizerWorkflowId = useRecoilComponentValue( + workflowVisualizerWorkflowIdComponentState, + ); + + const { isInRightDrawer } = useContext(ActionMenuContext); + + const setCommandMenuNavigationStack = useSetRecoilState( + commandMenuNavigationStackState, + ); + + return ( + } + onClick={() => { + if (!isInRightDrawer) { + setCommandMenuNavigationStack([]); + } + + if (!isDefined(workflowVisualizerWorkflowId)) { + return; + } + + openWorkflowTriggerTypeInCommandMenu(workflowVisualizerWorkflowId); + }} + /> + ); +}; diff --git a/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowDiagramEmptyTriggerReadonly.tsx b/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowDiagramEmptyTriggerReadonly.tsx new file mode 100644 index 0000000000..1a26f065cc --- /dev/null +++ b/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowDiagramEmptyTriggerReadonly.tsx @@ -0,0 +1,82 @@ +import { ActionMenuContext } from '@/action-menu/contexts/ActionMenuContext'; +import { useWorkflowCommandMenu } from '@/command-menu/hooks/useWorkflowCommandMenu'; +import { commandMenuNavigationStackState } from '@/command-menu/states/commandMenuNavigationStackState'; +import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue'; +import { useSetRecoilComponentState } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentState'; +import { workflowVisualizerWorkflowIdComponentState } from '@/workflow/states/workflowVisualizerWorkflowIdComponentState'; +import { workflowVisualizerWorkflowVersionIdComponentState } from '@/workflow/states/workflowVisualizerWorkflowVersionIdComponentState'; +import { WorkflowDiagramStepNodeBase } from '@/workflow/workflow-diagram/components/WorkflowDiagramStepNodeBase'; +import { workflowSelectedNodeComponentState } from '@/workflow/workflow-diagram/states/workflowSelectedNodeComponentState'; +import { TRIGGER_STEP_ID } from '@/workflow/workflow-trigger/constants/TriggerStepId'; +import styled from '@emotion/styled'; +import { useLingui } from '@lingui/react/macro'; +import { useContext } from 'react'; +import { useSetRecoilState } from 'recoil'; +import { isDefined } from 'twenty-shared/utils'; +import { useIcons } from 'twenty-ui/display'; + +const StyledStepNodeLabelIconContainer = styled.div` + align-items: center; + background: ${({ theme }) => theme.background.transparent.light}; + border-radius: ${({ theme }) => theme.spacing(1)}; + display: flex; + justify-content: center; + padding: ${({ theme }) => theme.spacing(3)}; +`; + +export const WorkflowDiagramEmptyTriggerReadonly = () => { + const { getIcon } = useIcons(); + const { t } = useLingui(); + + const workflowVisualizerWorkflowId = useRecoilComponentValue( + workflowVisualizerWorkflowIdComponentState, + ); + const workflowVisualizerWorkflowVersionId = useRecoilComponentValue( + workflowVisualizerWorkflowVersionIdComponentState, + ); + + const { isInRightDrawer } = useContext(ActionMenuContext); + + const { openWorkflowViewStepInCommandMenu } = useWorkflowCommandMenu(); + + const setWorkflowSelectedNode = useSetRecoilComponentState( + workflowSelectedNodeComponentState, + ); + + const setCommandMenuNavigationStack = useSetRecoilState( + commandMenuNavigationStackState, + ); + + return ( + } + onClick={() => { + if ( + !isDefined(workflowVisualizerWorkflowId) || + !isDefined(workflowVisualizerWorkflowVersionId) + ) { + throw new Error( + 'Workflow ID and Version ID must be defined to open the command menu.', + ); + } + + if (!isInRightDrawer) { + setCommandMenuNavigationStack([]); + } + + setWorkflowSelectedNode(TRIGGER_STEP_ID); + + openWorkflowViewStepInCommandMenu({ + workflowId: workflowVisualizerWorkflowId, + workflowVersionId: workflowVisualizerWorkflowVersionId, + title: t`Add a Trigger`, + icon: getIcon(null), + }); + }} + /> + ); +}; diff --git a/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowDiagramFilterEdgeEditable.tsx b/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowDiagramFilterEdgeEditable.tsx index 42ded5cd9b..7f17362082 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowDiagramFilterEdgeEditable.tsx +++ b/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowDiagramFilterEdgeEditable.tsx @@ -1,3 +1,5 @@ +import { ActionMenuContext } from '@/action-menu/contexts/ActionMenuContext'; +import { commandMenuNavigationStackState } from '@/command-menu/states/commandMenuNavigationStackState'; import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown'; import { DropdownContent } from '@/ui/layout/dropdown/components/DropdownContent'; import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer'; @@ -9,9 +11,12 @@ import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/ho import { useSetRecoilComponentState } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentState'; import { useWorkflowWithCurrentVersion } from '@/workflow/hooks/useWorkflowWithCurrentVersion'; import { workflowVisualizerWorkflowIdComponentState } from '@/workflow/states/workflowVisualizerWorkflowIdComponentState'; +import { WorkflowDiagramBaseEdge } from '@/workflow/workflow-diagram/components/WorkflowDiagramBaseEdge'; +import { WorkflowDiagramEdgeButtonGroup } from '@/workflow/workflow-diagram/components/WorkflowDiagramEdgeButtonGroup'; import { WorkflowDiagramEdgeV2Container } from '@/workflow/workflow-diagram/components/WorkflowDiagramEdgeV2Container'; import { WorkflowDiagramEdgeV2VisibilityContainer } from '@/workflow/workflow-diagram/components/WorkflowDiagramEdgeV2VisibilityContainer'; import { WORKFLOW_DIAGRAM_EDGE_OPTIONS_CLICK_OUTSIDE_ID } from '@/workflow/workflow-diagram/constants/WorkflowDiagramEdgeOptionsClickOutsideId'; +import { useEdgeHovered } from '@/workflow/workflow-diagram/hooks/useEdgeHovered'; import { useOpenWorkflowEditFilterInCommandMenu } from '@/workflow/workflow-diagram/hooks/useOpenWorkflowEditFilterInCommandMenu'; import { useStartNodeCreation } from '@/workflow/workflow-diagram/hooks/useStartNodeCreation'; import { workflowDiagramPanOnDragComponentState } from '@/workflow/workflow-diagram/states/workflowDiagramPanOnDragComponentState'; @@ -26,6 +31,8 @@ import { css } from '@emotion/react'; import styled from '@emotion/styled'; import { isNonEmptyString } from '@sniptt/guards'; import { EdgeLabelRenderer, EdgeProps, getBezierPath } from '@xyflow/react'; +import { useContext } from 'react'; +import { useSetRecoilState } from 'recoil'; import { isDefined } from 'twenty-shared/utils'; import { IconDotsVertical, @@ -35,9 +42,6 @@ import { } from 'twenty-ui/display'; import { IconButtonGroup } from 'twenty-ui/input'; import { MenuItem } from 'twenty-ui/navigation'; -import { useEdgeHovered } from '@/workflow/workflow-diagram/hooks/useEdgeHovered'; -import { WorkflowDiagramBaseEdge } from '@/workflow/workflow-diagram/components/WorkflowDiagramBaseEdge'; -import { WorkflowDiagramEdgeButtonGroup } from '@/workflow/workflow-diagram/components/WorkflowDiagramEdgeButtonGroup'; type WorkflowDiagramFilterEdgeEditableProps = EdgeProps; @@ -94,10 +98,15 @@ export const WorkflowDiagramFilterEdgeEditable = ({ workflowVisualizerWorkflowIdComponentState, ); const workflow = useWorkflowWithCurrentVersion(workflowVisualizerWorkflowId); + const { isInRightDrawer } = useContext(ActionMenuContext); const { deleteStep } = useDeleteStep({ workflow }); const { startNodeCreation, isNodeCreationStarted } = useStartNodeCreation(); + const setCommandMenuNavigationStack = useSetRecoilState( + commandMenuNavigationStackState, + ); + const { openDropdown } = useOpenDropdown(); const { closeDropdown } = useCloseDropdown(); @@ -130,6 +139,10 @@ export const WorkflowDiagramFilterEdgeEditable = ({ useOpenWorkflowEditFilterInCommandMenu(); const handleFilterButtonClick = () => { + if (!isInRightDrawer) { + setCommandMenuNavigationStack([]); + } + openWorkflowEditFilterInCommandMenu({ stepId: data.stepId, stepName: data.name, diff --git a/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowDiagramFilterEdgeReadonly.tsx b/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowDiagramFilterEdgeReadonly.tsx index e98a34b9d3..d2d44a1ae5 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowDiagramFilterEdgeReadonly.tsx +++ b/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowDiagramFilterEdgeReadonly.tsx @@ -1,3 +1,5 @@ +import { ActionMenuContext } from '@/action-menu/contexts/ActionMenuContext'; +import { commandMenuNavigationStackState } from '@/command-menu/states/commandMenuNavigationStackState'; import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue'; import { WorkflowDiagramEdgeV2Container } from '@/workflow/workflow-diagram/components/WorkflowDiagramEdgeV2Container'; import { WorkflowDiagramEdgeV2VisibilityContainer } from '@/workflow/workflow-diagram/components/WorkflowDiagramEdgeV2VisibilityContainer'; @@ -18,6 +20,8 @@ import { EdgeProps, getBezierPath, } from '@xyflow/react'; +import { useContext } from 'react'; +import { useSetRecoilState } from 'recoil'; import { IconFilter } from 'twenty-ui/display'; import { IconButtonGroup } from 'twenty-ui/input'; @@ -62,6 +66,8 @@ export const WorkflowDiagramFilterEdgeReadonly = ({ }: WorkflowDiagramFilterEdgeReadonlyProps) => { assertFilterEdgeDataOrThrow(data); + const { isInRightDrawer } = useContext(ActionMenuContext); + const theme = useTheme(); const [edgePath, labelX, labelY] = getBezierPath({ @@ -81,7 +87,15 @@ export const WorkflowDiagramFilterEdgeReadonly = ({ const { openWorkflowViewFilterInCommandMenu } = useOpenWorkflowViewFilterInCommandMenu(); + const setCommandMenuNavigationStack = useSetRecoilState( + commandMenuNavigationStackState, + ); + const handleFilterButtonClick = () => { + if (!isInRightDrawer) { + setCommandMenuNavigationStack([]); + } + openWorkflowViewFilterInCommandMenu({ stepId: data.stepId, stepName: data.name, diff --git a/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowDiagramStepNodeBase.tsx b/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowDiagramStepNodeBase.tsx index 24878eeb32..2d71aa06e1 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowDiagramStepNodeBase.tsx +++ b/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowDiagramStepNodeBase.tsx @@ -150,6 +150,7 @@ export const WorkflowDiagramStepNodeBase = ({ RightFloatingElement, BottomHoverFloatingElement, displayHandle = true, + onClick, onMouseEnter, onMouseLeave, }: { @@ -161,6 +162,7 @@ export const WorkflowDiagramStepNodeBase = ({ RightFloatingElement?: React.ReactNode; BottomHoverFloatingElement?: React.ReactNode; displayHandle?: boolean; + onClick?: () => void; onMouseEnter?: (event: MouseEvent) => void; onMouseLeave?: (event: MouseEvent) => void; }) => { @@ -172,6 +174,7 @@ export const WorkflowDiagramStepNodeBase = ({ diff --git a/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowDiagramStepNodeEditable.tsx b/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowDiagramStepNodeEditable.tsx index 323f40e9d0..c8b0a06bc1 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowDiagramStepNodeEditable.tsx +++ b/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowDiagramStepNodeEditable.tsx @@ -1,10 +1,20 @@ +import { ActionMenuContext } from '@/action-menu/contexts/ActionMenuContext'; +import { useWorkflowCommandMenu } from '@/command-menu/hooks/useWorkflowCommandMenu'; +import { commandMenuNavigationStackState } from '@/command-menu/states/commandMenuNavigationStackState'; import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue'; +import { useSetRecoilComponentState } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentState'; import { useWorkflowWithCurrentVersion } from '@/workflow/hooks/useWorkflowWithCurrentVersion'; import { workflowVisualizerWorkflowIdComponentState } from '@/workflow/states/workflowVisualizerWorkflowIdComponentState'; import { assertWorkflowWithCurrentVersionIsDefined } from '@/workflow/utils/assertWorkflowWithCurrentVersionIsDefined'; import { WorkflowDiagramStepNodeEditableContent } from '@/workflow/workflow-diagram/components/WorkflowDiagramStepNodeEditableContent'; +import { workflowSelectedNodeComponentState } from '@/workflow/workflow-diagram/states/workflowSelectedNodeComponentState'; import { WorkflowDiagramStepNodeData } from '@/workflow/workflow-diagram/types/WorkflowDiagram'; +import { getWorkflowNodeIconKey } from '@/workflow/workflow-diagram/utils/getWorkflowNodeIconKey'; import { useDeleteStep } from '@/workflow/workflow-steps/hooks/useDeleteStep'; +import { useContext } from 'react'; +import { useSetRecoilState } from 'recoil'; +import { isDefined } from 'twenty-shared/utils'; +import { useIcons } from 'twenty-ui/display'; export const WorkflowDiagramStepNodeEditable = ({ id, @@ -15,6 +25,8 @@ export const WorkflowDiagramStepNodeEditable = ({ data: WorkflowDiagramStepNodeData; selected?: boolean; }) => { + const { getIcon } = useIcons(); + const workflowVisualizerWorkflowId = useRecoilComponentValue( workflowVisualizerWorkflowIdComponentState, ); @@ -28,12 +40,41 @@ export const WorkflowDiagramStepNodeEditable = ({ workflow: workflowWithCurrentVersion, }); + const setWorkflowSelectedNode = useSetRecoilComponentState( + workflowSelectedNodeComponentState, + ); + + const { openWorkflowEditStepInCommandMenu } = useWorkflowCommandMenu(); + + const { isInRightDrawer } = useContext(ActionMenuContext); + + const setCommandMenuNavigationStack = useSetRecoilState( + commandMenuNavigationStackState, + ); + return ( { + if (!isInRightDrawer) { + setCommandMenuNavigationStack([]); + } + + setWorkflowSelectedNode(id); + + if (isDefined(workflowVisualizerWorkflowId)) { + openWorkflowEditStepInCommandMenu( + workflowVisualizerWorkflowId, + data.name, + getIcon(getWorkflowNodeIconKey(data)), + ); + + return; + } + }} onDelete={() => { deleteStep(id); }} diff --git a/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowDiagramStepNodeEditableContent.tsx b/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowDiagramStepNodeEditableContent.tsx index 862ca7c126..2c46b78cc9 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowDiagramStepNodeEditableContent.tsx +++ b/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowDiagramStepNodeEditableContent.tsx @@ -1,13 +1,13 @@ +import { WorkflowDiagramCreateStepElement } from '@/workflow/workflow-diagram/components/WorkflowDiagramCreateStepElement'; import { WorkflowDiagramStepNodeBase } from '@/workflow/workflow-diagram/components/WorkflowDiagramStepNodeBase'; import { WorkflowDiagramStepNodeIcon } from '@/workflow/workflow-diagram/components/WorkflowDiagramStepNodeIcon'; +import { useStartNodeCreation } from '@/workflow/workflow-diagram/hooks/useStartNodeCreation'; import { WorkflowDiagramStepNodeData } from '@/workflow/workflow-diagram/types/WorkflowDiagram'; import { WorkflowDiagramNodeVariant } from '@/workflow/workflow-diagram/types/WorkflowDiagramNodeVariant'; -import { FloatingIconButton } from 'twenty-ui/input'; -import { IconTrash } from 'twenty-ui/display'; -import { WorkflowDiagramCreateStepElement } from '@/workflow/workflow-diagram/components/WorkflowDiagramCreateStepElement'; -import React, { useState } from 'react'; import styled from '@emotion/styled'; -import { useStartNodeCreation } from '@/workflow/workflow-diagram/hooks/useStartNodeCreation'; +import { useState } from 'react'; +import { IconTrash } from 'twenty-ui/display'; +import { FloatingIconButton } from 'twenty-ui/input'; const StyledDeleteButtonContainer = styled.div` display: flex; @@ -39,12 +39,14 @@ export const WorkflowDiagramStepNodeEditableContent = ({ selected, variant, onDelete, + onClick, }: { id: string; data: WorkflowDiagramStepNodeData; variant: WorkflowDiagramNodeVariant; selected: boolean; onDelete: () => void; + onClick?: () => void; }) => { const [isHovered, setIsHovered] = useState(false); @@ -60,6 +62,7 @@ export const WorkflowDiagramStepNodeEditableContent = ({ { + const { getIcon } = useIcons(); + + const workflowVisualizerWorkflowId = useRecoilComponentValue( + workflowVisualizerWorkflowIdComponentState, + ); + const workflowVisualizerWorkflowVersionId = useRecoilComponentValue( + workflowVisualizerWorkflowVersionIdComponentState, + ); + + const setWorkflowSelectedNode = useSetRecoilComponentState( + workflowSelectedNodeComponentState, + ); + + const { openWorkflowViewStepInCommandMenu } = useWorkflowCommandMenu(); + + const { isInRightDrawer } = useContext(ActionMenuContext); + + const setCommandMenuNavigationStack = useSetRecoilState( + commandMenuNavigationStackState, + ); + return ( } displayHandle={false} + onClick={() => { + if ( + !isDefined(workflowVisualizerWorkflowId) || + !isDefined(workflowVisualizerWorkflowVersionId) + ) { + throw new Error('Workflow ID and Version ID must be defined'); + } + + if (!isInRightDrawer) { + setCommandMenuNavigationStack([]); + } + + setWorkflowSelectedNode(id); + + openWorkflowViewStepInCommandMenu({ + workflowId: workflowVisualizerWorkflowId, + workflowVersionId: workflowVisualizerWorkflowVersionId, + title: data.name, + icon: getIcon(getWorkflowNodeIconKey(data)), + }); + }} /> ); }; diff --git a/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowRunDiagramCanvas.tsx b/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowRunDiagramCanvas.tsx index ecb37618ba..dc6a679082 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowRunDiagramCanvas.tsx +++ b/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowRunDiagramCanvas.tsx @@ -3,9 +3,7 @@ import { WorkflowDiagramCanvasBase } from '@/workflow/workflow-diagram/component import { WorkflowDiagramDefaultEdgeRun } from '@/workflow/workflow-diagram/components/WorkflowDiagramDefaultEdgeRun'; import { WorkflowDiagramFilterEdgeRun } from '@/workflow/workflow-diagram/components/WorkflowDiagramFilterEdgeRun'; import { WorkflowDiagramFilteringDisabledEdgeRun } from '@/workflow/workflow-diagram/components/WorkflowDiagramFilteringDisabledEdgeRun'; -import { WorkflowDiagramStepNodeReadonly } from '@/workflow/workflow-diagram/components/WorkflowDiagramStepNodeReadonly'; -import { WorkflowRunDiagramCanvasEffect } from '@/workflow/workflow-diagram/components/WorkflowRunDiagramCanvasEffect'; -import { useHandleWorkflowRunDiagramCanvasInit } from '@/workflow/workflow-diagram/hooks/useHandleWorkflowRunDiagramCanvasInit'; +import { WorkflowRunDiagramStepNode } from '@/workflow/workflow-diagram/components/WorkflowRunDiagramStepNode'; import { getWorkflowRunStatusTagProps } from '@/workflow/workflow-diagram/utils/getWorkflowRunStatusTagProps'; import { ReactFlowProvider } from '@xyflow/react'; @@ -18,14 +16,11 @@ export const WorkflowRunDiagramCanvas = ({ workflowRunStatus, }); - const { handleWorkflowRunDiagramCanvasInit } = - useHandleWorkflowRunDiagramCanvasInit(); - return ( - - ); }; diff --git a/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowRunDiagramCanvasEffect.tsx b/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowRunDiagramCanvasEffect.tsx deleted file mode 100644 index e2f2047951..0000000000 --- a/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowRunDiagramCanvasEffect.tsx +++ /dev/null @@ -1,91 +0,0 @@ -import { useWorkflowCommandMenu } from '@/command-menu/hooks/useWorkflowCommandMenu'; -import { useRecoilComponentCallbackState } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackState'; -import { getSnapshotValue } from '@/ui/utilities/state/utils/getSnapshotValue'; -import { useWorkflowRunIdOrThrow } from '@/workflow/hooks/useWorkflowRunIdOrThrow'; -import { workflowVisualizerWorkflowIdComponentState } from '@/workflow/states/workflowVisualizerWorkflowIdComponentState'; -import { workflowDiagramStatusComponentState } from '@/workflow/workflow-diagram/states/workflowDiagramStatusComponentState'; -import { workflowSelectedNodeComponentState } from '@/workflow/workflow-diagram/states/workflowSelectedNodeComponentState'; -import { WorkflowRunDiagramNode } from '@/workflow/workflow-diagram/types/WorkflowDiagram'; -import { getWorkflowNodeIconKey } from '@/workflow/workflow-diagram/utils/getWorkflowNodeIconKey'; -import { OnSelectionChangeParams, useOnSelectionChange } from '@xyflow/react'; -import { useRecoilCallback } from 'recoil'; -import { isDefined } from 'twenty-shared/utils'; -import { useIcons } from 'twenty-ui/display'; - -export const WorkflowRunDiagramCanvasEffect = () => { - const { getIcon } = useIcons(); - - const { openWorkflowRunViewStepInCommandMenu } = useWorkflowCommandMenu(); - - const workflowRunId = useWorkflowRunIdOrThrow(); - - const workflowVisualizerWorkflowIdState = useRecoilComponentCallbackState( - workflowVisualizerWorkflowIdComponentState, - ); - const workflowDiagramStatusState = useRecoilComponentCallbackState( - workflowDiagramStatusComponentState, - ); - const workflowSelectedNodeState = useRecoilComponentCallbackState( - workflowSelectedNodeComponentState, - ); - - const handleSelectionChange = useRecoilCallback( - ({ snapshot, set }) => - ({ nodes }: OnSelectionChangeParams) => { - const workflowId = getSnapshotValue( - snapshot, - workflowVisualizerWorkflowIdState, - ); - - if (!isDefined(workflowId)) { - throw new Error('Expected the workflowId to be defined.'); - } - - const workflowDiagramStatus = getSnapshotValue( - snapshot, - workflowDiagramStatusState, - ); - - // The `handleSelectionChange` function is called when the diagram initializes and - // a node is selected. In this case, we don't want to execute the rest of this function. - // We open the Side PanelĀ® synchronously after ReactFlow is initialized and a node is selected, - // animations perform better that way. - if (workflowDiagramStatus !== 'done') { - return; - } - - const selectedNode = nodes[0] as WorkflowRunDiagramNode | undefined; - - if (!isDefined(selectedNode)) { - return; - } - - set(workflowSelectedNodeState, selectedNode.id); - - const selectedNodeData = selectedNode.data; - - openWorkflowRunViewStepInCommandMenu({ - workflowId, - workflowRunId, - title: selectedNodeData.name, - icon: getIcon(getWorkflowNodeIconKey(selectedNodeData)), - workflowSelectedNode: selectedNode.id, - stepExecutionStatus: selectedNodeData.runStatus, - }); - }, - [ - workflowVisualizerWorkflowIdState, - workflowDiagramStatusState, - workflowSelectedNodeState, - openWorkflowRunViewStepInCommandMenu, - workflowRunId, - getIcon, - ], - ); - - useOnSelectionChange({ - onChange: handleSelectionChange, - }); - - return null; -}; diff --git a/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowRunDiagramStepNode.tsx b/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowRunDiagramStepNode.tsx new file mode 100644 index 0000000000..791bba9fef --- /dev/null +++ b/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowRunDiagramStepNode.tsx @@ -0,0 +1,75 @@ +import { ActionMenuContext } from '@/action-menu/contexts/ActionMenuContext'; +import { useWorkflowCommandMenu } from '@/command-menu/hooks/useWorkflowCommandMenu'; +import { commandMenuNavigationStackState } from '@/command-menu/states/commandMenuNavigationStackState'; +import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue'; +import { useSetRecoilComponentState } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentState'; +import { useWorkflowRunIdOrThrow } from '@/workflow/hooks/useWorkflowRunIdOrThrow'; +import { workflowVisualizerWorkflowIdComponentState } from '@/workflow/states/workflowVisualizerWorkflowIdComponentState'; +import { WorkflowDiagramStepNodeBase } from '@/workflow/workflow-diagram/components/WorkflowDiagramStepNodeBase'; +import { WorkflowDiagramStepNodeIcon } from '@/workflow/workflow-diagram/components/WorkflowDiagramStepNodeIcon'; +import { workflowSelectedNodeComponentState } from '@/workflow/workflow-diagram/states/workflowSelectedNodeComponentState'; +import { WorkflowRunDiagramStepNodeData } from '@/workflow/workflow-diagram/types/WorkflowDiagram'; +import { getNodeVariantFromStepRunStatus } from '@/workflow/workflow-diagram/utils/getNodeVariantFromStepRunStatus'; +import { getWorkflowNodeIconKey } from '@/workflow/workflow-diagram/utils/getWorkflowNodeIconKey'; +import { useContext } from 'react'; +import { useSetRecoilState } from 'recoil'; +import { isDefined } from 'twenty-shared/utils'; +import { useIcons } from 'twenty-ui/display'; + +export const WorkflowRunDiagramStepNode = ({ + id, + data, +}: { + id: string; + data: WorkflowRunDiagramStepNodeData; +}) => { + const { getIcon } = useIcons(); + + const workflowId = useRecoilComponentValue( + workflowVisualizerWorkflowIdComponentState, + ); + const workflowRunId = useWorkflowRunIdOrThrow(); + + const setWorkflowSelectedNode = useSetRecoilComponentState( + workflowSelectedNodeComponentState, + ); + + const { openWorkflowRunViewStepInCommandMenu } = useWorkflowCommandMenu(); + + const { isInRightDrawer } = useContext(ActionMenuContext); + + const setCommandMenuNavigationStack = useSetRecoilState( + commandMenuNavigationStackState, + ); + + return ( + } + displayHandle={false} + onClick={() => { + if (!isDefined(workflowId)) { + throw new Error('Workflow ID must be defined'); + } + + if (!isInRightDrawer) { + setCommandMenuNavigationStack([]); + } + + setWorkflowSelectedNode(id); + + openWorkflowRunViewStepInCommandMenu({ + workflowId, + workflowRunId, + title: data.name, + icon: getIcon(getWorkflowNodeIconKey(data)), + workflowSelectedNode: id, + stepExecutionStatus: data.runStatus, + }); + }} + /> + ); +}; diff --git a/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowRunVisualizerEffect.tsx b/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowRunVisualizerEffect.tsx index 446706d8c8..47320367a1 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowRunVisualizerEffect.tsx +++ b/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowRunVisualizerEffect.tsx @@ -13,7 +13,6 @@ import { WorkflowRunState } from '@/workflow/types/Workflow'; import { workflowDiagramComponentState } from '@/workflow/workflow-diagram/states/workflowDiagramComponentState'; import { workflowDiagramStatusComponentState } from '@/workflow/workflow-diagram/states/workflowDiagramStatusComponentState'; import { workflowRunDiagramAutomaticallyOpenedStepsComponentState } from '@/workflow/workflow-diagram/states/workflowRunDiagramAutomaticallyOpenedStepsComponentState'; -import { workflowRunStepToOpenByDefaultComponentState } from '@/workflow/workflow-diagram/states/workflowRunStepToOpenByDefaultComponentState'; import { workflowSelectedNodeComponentState } from '@/workflow/workflow-diagram/states/workflowSelectedNodeComponentState'; import { generateWorkflowRunDiagram } from '@/workflow/workflow-diagram/utils/generateWorkflowRunDiagram'; import { getWorkflowNodeIconKey } from '@/workflow/workflow-diagram/utils/getWorkflowNodeIconKey'; @@ -52,9 +51,6 @@ export const WorkflowRunVisualizerEffect = ({ const workflowDiagramStatusState = useRecoilComponentCallbackState( workflowDiagramStatusComponentState, ); - const workflowRunStepToOpenByDefaultState = useRecoilComponentCallbackState( - workflowRunStepToOpenByDefaultComponentState, - ); const workflowSelectedNodeState = useRecoilComponentCallbackState( workflowSelectedNodeComponentState, ); @@ -126,67 +122,70 @@ export const WorkflowRunVisualizerEffect = ({ isWorkflowFilteringEnabled, }); - if (isDefined(stepToOpenByDefault)) { - if (isInRightDrawer) { - set(workflowDiagramState, baseWorkflowRunDiagram); - - const workflowRunDiagramAutomaticallyOpenedSteps = getSnapshotValue( - snapshot, - workflowRunDiagramAutomaticallyOpenedStepsState, - ); - const hasStepAlreadyBeenOpenedAutomatically = - workflowRunDiagramAutomaticallyOpenedSteps.includes( - stepToOpenByDefault.id, - ); - - if ( - workflowDiagramStatus === 'done' && - !hasStepAlreadyBeenOpenedAutomatically - ) { - set(workflowSelectedNodeState, stepToOpenByDefault.id); - - const workflowVisualizerWorkflowId = getSnapshotValue( - snapshot, - workflowVisualizerWorkflowIdState, - ); - if (!isDefined(workflowVisualizerWorkflowId)) { - throw new Error( - 'The workflow id must be set; ensure the workflow id is always set before rendering the workflow diagram.', - ); - } - - set(workflowRunDiagramAutomaticallyOpenedStepsState, [ - ...workflowRunDiagramAutomaticallyOpenedSteps, - stepToOpenByDefault.id, - ]); - openWorkflowRunViewStepInCommandMenu({ - workflowId: workflowVisualizerWorkflowId, - workflowRunId, - title: stepToOpenByDefault.data.name, - icon: getIcon(getWorkflowNodeIconKey(stepToOpenByDefault.data)), - workflowSelectedNode: stepToOpenByDefault.id, - stepExecutionStatus: stepToOpenByDefault.data.runStatus, - }); - } - } else { - const workflowRunDiagram = selectWorkflowDiagramNode({ - diagram: baseWorkflowRunDiagram, - nodeIdToSelect: stepToOpenByDefault.id, - }); - - set(workflowDiagramState, workflowRunDiagram); - set(workflowRunStepToOpenByDefaultState, { - id: stepToOpenByDefault.id, - data: stepToOpenByDefault.data, - }); - } - } else { - set(workflowDiagramState, baseWorkflowRunDiagram); - } - if (workflowDiagramStatus !== 'done') { set(workflowDiagramStatusState, 'computing-dimensions'); } + + if (!isDefined(stepToOpenByDefault)) { + set(workflowDiagramState, baseWorkflowRunDiagram); + + return; + } + + const workflowRunDiagramAutomaticallyOpenedSteps = getSnapshotValue( + snapshot, + workflowRunDiagramAutomaticallyOpenedStepsState, + ); + const hasStepAlreadyBeenOpenedAutomatically = + workflowRunDiagramAutomaticallyOpenedSteps.some( + (step) => + step.stepId === stepToOpenByDefault.id && + step.isInRightDrawer === isInRightDrawer, + ); + + const workflowVisualizerWorkflowId = getSnapshotValue( + snapshot, + workflowVisualizerWorkflowIdState, + ); + if (!isDefined(workflowVisualizerWorkflowId)) { + throw new Error( + 'The workflow id must be set; ensure the workflow id is always set before rendering the workflow diagram.', + ); + } + + if (isInRightDrawer) { + set(workflowDiagramState, baseWorkflowRunDiagram); + } else { + const workflowRunDiagram = selectWorkflowDiagramNode({ + diagram: baseWorkflowRunDiagram, + nodeIdToSelect: stepToOpenByDefault.id, + }); + + set(workflowDiagramState, workflowRunDiagram); + } + + if (hasStepAlreadyBeenOpenedAutomatically) { + return; + } + + set(workflowSelectedNodeState, stepToOpenByDefault.id); + + set(workflowRunDiagramAutomaticallyOpenedStepsState, [ + ...workflowRunDiagramAutomaticallyOpenedSteps, + { + stepId: stepToOpenByDefault.id, + isInRightDrawer, + }, + ]); + + openWorkflowRunViewStepInCommandMenu({ + workflowId: workflowVisualizerWorkflowId, + workflowRunId, + title: stepToOpenByDefault.data.name, + icon: getIcon(getWorkflowNodeIconKey(stepToOpenByDefault.data)), + workflowSelectedNode: stepToOpenByDefault.id, + stepExecutionStatus: stepToOpenByDefault.data.runStatus, + }); }, [ flowState, @@ -197,7 +196,6 @@ export const WorkflowRunVisualizerEffect = ({ workflowDiagramStatusState, workflowRunDiagramAutomaticallyOpenedStepsState, workflowRunId, - workflowRunStepToOpenByDefaultState, workflowSelectedNodeState, workflowVisualizerWorkflowIdState, ], diff --git a/packages/twenty-front/src/modules/workflow/workflow-diagram/components/__stories__/WorkflowDiagramEmptyTrigger.stories.tsx b/packages/twenty-front/src/modules/workflow/workflow-diagram/components/__stories__/WorkflowDiagramEmptyTriggerEditable.stories.tsx similarity index 76% rename from packages/twenty-front/src/modules/workflow/workflow-diagram/components/__stories__/WorkflowDiagramEmptyTrigger.stories.tsx rename to packages/twenty-front/src/modules/workflow/workflow-diagram/components/__stories__/WorkflowDiagramEmptyTriggerEditable.stories.tsx index 0960cf8054..9b2ada6377 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-diagram/components/__stories__/WorkflowDiagramEmptyTrigger.stories.tsx +++ b/packages/twenty-front/src/modules/workflow/workflow-diagram/components/__stories__/WorkflowDiagramEmptyTriggerEditable.stories.tsx @@ -1,24 +1,26 @@ import { Meta, StoryObj } from '@storybook/react'; -import '@xyflow/react/dist/style.css'; -import { ReactflowDecorator } from '~/testing/decorators/ReactflowDecorator'; -import { WorkflowDiagramEmptyTrigger } from '../WorkflowDiagramEmptyTrigger'; -import { ComponentDecorator } from 'twenty-ui/testing'; -import { RecoilRoot } from 'recoil'; import { WorkflowVisualizerComponentInstanceContext } from '@/workflow/workflow-diagram/states/contexts/WorkflowVisualizerComponentInstanceContext'; +import '@xyflow/react/dist/style.css'; +import { RecoilRoot } from 'recoil'; +import { ComponentDecorator } from 'twenty-ui/testing'; +import { I18nFrontDecorator } from '~/testing/decorators/I18nFrontDecorator'; +import { ReactflowDecorator } from '~/testing/decorators/ReactflowDecorator'; +import { WorkflowDiagramEmptyTriggerEditable } from '../WorkflowDiagramEmptyTriggerEditable'; -const meta: Meta = { - title: 'Modules/Workflow/WorkflowDiagramEmptyTrigger', - component: WorkflowDiagramEmptyTrigger, +const meta: Meta = { + title: 'Modules/Workflow/WorkflowDiagramEmptyTriggerEditable', + component: WorkflowDiagramEmptyTriggerEditable, args: { data: { nodeType: 'empty-trigger', }, }, + decorators: [I18nFrontDecorator], }; export default meta; -type Story = StoryObj; +type Story = StoryObj; export const Default: Story = { decorators: [ diff --git a/packages/twenty-front/src/modules/workflow/workflow-diagram/constants/CreateStepNodeWidth.ts b/packages/twenty-front/src/modules/workflow/workflow-diagram/constants/CreateStepNodeWidth.ts deleted file mode 100644 index c03e587e72..0000000000 --- a/packages/twenty-front/src/modules/workflow/workflow-diagram/constants/CreateStepNodeWidth.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { NODE_BORDER_WIDTH } from '@/workflow/workflow-diagram/constants/NodeBorderWidth'; - -export const STEP_ICON_WIDTH = 24; - -const STEP_PADDING = 8; - -export const CREATE_STEP_NODE_WIDTH = - STEP_ICON_WIDTH + STEP_PADDING + NODE_BORDER_WIDTH; diff --git a/packages/twenty-front/src/modules/workflow/workflow-diagram/hooks/__tests__/useTriggerNodeSelection.test.tsx b/packages/twenty-front/src/modules/workflow/workflow-diagram/hooks/__tests__/useTriggerNodeSelection.test.tsx deleted file mode 100644 index 40fb6ca1a3..0000000000 --- a/packages/twenty-front/src/modules/workflow/workflow-diagram/hooks/__tests__/useTriggerNodeSelection.test.tsx +++ /dev/null @@ -1,79 +0,0 @@ -import { useRecoilComponentState } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentState'; -import { useTriggerNodeSelection } from '@/workflow/workflow-diagram/hooks/useTriggerNodeSelection'; -import { WorkflowVisualizerComponentInstanceContext } from '@/workflow/workflow-diagram/states/contexts/WorkflowVisualizerComponentInstanceContext'; -import { workflowDiagramTriggerNodeSelectionComponentState } from '@/workflow/workflow-diagram/states/workflowDiagramTriggerNodeSelectionComponentState'; -import { act, renderHook } from '@testing-library/react'; -import { useReactFlow } from '@xyflow/react'; -import { RecoilRoot } from 'recoil'; - -jest.mock('@xyflow/react', () => ({ - useReactFlow: jest.fn(), -})); - -const wrapper = ({ children }: { children: React.ReactNode }) => ( - - - {children} - - -); - -describe('useTriggerNodeSelection', () => { - const mockSetNodes = jest.fn(); - - beforeEach(() => { - (useReactFlow as jest.Mock).mockReturnValue({ - setNodes: mockSetNodes, - }); - }); - - afterEach(() => { - jest.clearAllMocks(); - }); - - it('should trigger node selection', () => { - const { result } = renderHook( - () => { - const [ - workflowDiagramTriggerNodeSelection, - setWorkflowDiagramTriggerNodeSelection, - ] = useRecoilComponentState( - workflowDiagramTriggerNodeSelectionComponentState, - ); - - useTriggerNodeSelection(); - - return { - workflowDiagramTriggerNodeSelection, - setWorkflowDiagramTriggerNodeSelection, - }; - }, - { - wrapper, - }, - ); - - const mockNodeId = 'test-node-id'; - - act(() => { - result.current.setWorkflowDiagramTriggerNodeSelection(mockNodeId); - }); - - expect(result.current.workflowDiagramTriggerNodeSelection).toBeUndefined(); - - expect(mockSetNodes).toHaveBeenCalled(); - }); - - it('should not trigger update if state is not defined', () => { - renderHook(() => useTriggerNodeSelection(), { - wrapper, - }); - - // Ensure updateNode is not called when state is undefined - expect(mockSetNodes).not.toHaveBeenCalled(); - }); -}); diff --git a/packages/twenty-front/src/modules/workflow/workflow-diagram/hooks/useHandleWorkflowRunDiagramCanvasInit.ts b/packages/twenty-front/src/modules/workflow/workflow-diagram/hooks/useHandleWorkflowRunDiagramCanvasInit.ts deleted file mode 100644 index 5167931319..0000000000 --- a/packages/twenty-front/src/modules/workflow/workflow-diagram/hooks/useHandleWorkflowRunDiagramCanvasInit.ts +++ /dev/null @@ -1,124 +0,0 @@ -import { ActionMenuContext } from '@/action-menu/contexts/ActionMenuContext'; -import { useWorkflowCommandMenu } from '@/command-menu/hooks/useWorkflowCommandMenu'; -import { useRecoilComponentCallbackState } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackState'; -import { getSnapshotValue } from '@/ui/utilities/state/utils/getSnapshotValue'; -import { useWorkflowRunIdOrThrow } from '@/workflow/hooks/useWorkflowRunIdOrThrow'; -import { workflowVisualizerWorkflowIdComponentState } from '@/workflow/states/workflowVisualizerWorkflowIdComponentState'; -import { workflowDiagramStatusComponentState } from '@/workflow/workflow-diagram/states/workflowDiagramStatusComponentState'; -import { workflowRunDiagramAutomaticallyOpenedStepsComponentState } from '@/workflow/workflow-diagram/states/workflowRunDiagramAutomaticallyOpenedStepsComponentState'; -import { workflowRunStepToOpenByDefaultComponentState } from '@/workflow/workflow-diagram/states/workflowRunStepToOpenByDefaultComponentState'; -import { workflowSelectedNodeComponentState } from '@/workflow/workflow-diagram/states/workflowSelectedNodeComponentState'; -import { getWorkflowNodeIconKey } from '@/workflow/workflow-diagram/utils/getWorkflowNodeIconKey'; -import { useContext } from 'react'; -import { useRecoilCallback } from 'recoil'; -import { isDefined } from 'twenty-shared/utils'; -import { useIcons } from 'twenty-ui/display'; - -export const useHandleWorkflowRunDiagramCanvasInit = () => { - const { getIcon } = useIcons(); - - const { openWorkflowRunViewStepInCommandMenu } = useWorkflowCommandMenu(); - const { isInRightDrawer } = useContext(ActionMenuContext); - - const workflowRunId = useWorkflowRunIdOrThrow(); - - const workflowVisualizerWorkflowIdState = useRecoilComponentCallbackState( - workflowVisualizerWorkflowIdComponentState, - ); - const workflowDiagramStatusState = useRecoilComponentCallbackState( - workflowDiagramStatusComponentState, - ); - const workflowRunStepToOpenByDefaultState = useRecoilComponentCallbackState( - workflowRunStepToOpenByDefaultComponentState, - ); - const workflowSelectedNodeState = useRecoilComponentCallbackState( - workflowSelectedNodeComponentState, - ); - const workflowRunDiagramAutomaticallyOpenedStepsState = - useRecoilComponentCallbackState( - workflowRunDiagramAutomaticallyOpenedStepsComponentState, - ); - - const handleWorkflowRunDiagramCanvasInit = useRecoilCallback( - ({ snapshot, set }) => - () => { - const workflowDiagramStatus = getSnapshotValue( - snapshot, - workflowDiagramStatusState, - ); - - if (workflowDiagramStatus !== 'computing-dimensions') { - throw new Error( - 'Sequence error: reactflow should be considered initialized only when the workflow diagram status is computing-dimensions.', - ); - } - - set(workflowDiagramStatusState, 'done'); - - if (isInRightDrawer) { - return; - } - - const workflowStepToOpenByDefault = getSnapshotValue( - snapshot, - workflowRunStepToOpenByDefaultState, - ); - - if (isDefined(workflowStepToOpenByDefault)) { - const workflowVisualizerWorkflowId = getSnapshotValue( - snapshot, - workflowVisualizerWorkflowIdState, - ); - if (!isDefined(workflowVisualizerWorkflowId)) { - throw new Error( - 'The workflow id must be set; ensure the workflow id is always set before rendering the workflow diagram.', - ); - } - - set(workflowSelectedNodeState, workflowStepToOpenByDefault.id); - - const workflowRunDiagramAutomaticallyOpenedSteps = getSnapshotValue( - snapshot, - workflowRunDiagramAutomaticallyOpenedStepsState, - ); - const hasStepAlreadyBeenOpenedAutomatically = - workflowRunDiagramAutomaticallyOpenedSteps.includes( - workflowStepToOpenByDefault.id, - ); - - // FIXME: This is a workaround to avoid opening a workflow run step twice when going from the side panel to the fullscreen show page. - // The step is opened in the `handleSelectionChange` function of `WorkflowRunDiagramCanvasEffect`. I think it shouldn't be opened there but - // we should keep opening the step here, in `handleWorkflowRunDiagramCanvasInit`. - if (!hasStepAlreadyBeenOpenedAutomatically) { - openWorkflowRunViewStepInCommandMenu({ - workflowId: workflowVisualizerWorkflowId, - workflowRunId, - title: workflowStepToOpenByDefault.data.name, - icon: getIcon( - getWorkflowNodeIconKey(workflowStepToOpenByDefault.data), - ), - workflowSelectedNode: workflowStepToOpenByDefault.id, - stepExecutionStatus: workflowStepToOpenByDefault.data.runStatus, - }); - } - - set(workflowRunStepToOpenByDefaultState, undefined); - } - }, - [ - workflowDiagramStatusState, - isInRightDrawer, - workflowRunStepToOpenByDefaultState, - workflowVisualizerWorkflowIdState, - workflowSelectedNodeState, - workflowRunDiagramAutomaticallyOpenedStepsState, - openWorkflowRunViewStepInCommandMenu, - workflowRunId, - getIcon, - ], - ); - - return { - handleWorkflowRunDiagramCanvasInit, - }; -}; diff --git a/packages/twenty-front/src/modules/workflow/workflow-diagram/hooks/useStartNodeCreation.ts b/packages/twenty-front/src/modules/workflow/workflow-diagram/hooks/useStartNodeCreation.ts index ea9c17811f..9c7bfe443d 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-diagram/hooks/useStartNodeCreation.ts +++ b/packages/twenty-front/src/modules/workflow/workflow-diagram/hooks/useStartNodeCreation.ts @@ -1,13 +1,17 @@ -import { useCallback } from 'react'; - +import { ActionMenuContext } from '@/action-menu/contexts/ActionMenuContext'; import { useWorkflowCommandMenu } from '@/command-menu/hooks/useWorkflowCommandMenu'; +import { commandMenuNavigationStackState } from '@/command-menu/states/commandMenuNavigationStackState'; import { useRecoilComponentState } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentState'; import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue'; import { workflowVisualizerWorkflowIdComponentState } from '@/workflow/states/workflowVisualizerWorkflowIdComponentState'; import { workflowInsertStepIdsComponentState } from '@/workflow/workflow-steps/states/workflowInsertStepIdsComponentState'; +import { useCallback, useContext } from 'react'; +import { useSetRecoilState } from 'recoil'; import { isDefined } from 'twenty-shared/utils'; export const useStartNodeCreation = () => { + const { isInRightDrawer } = useContext(ActionMenuContext); + const [workflowInsertStepIds, setWorkflowInsertStepIds] = useRecoilComponentState(workflowInsertStepIdsComponentState); @@ -17,6 +21,10 @@ export const useStartNodeCreation = () => { workflowVisualizerWorkflowIdComponentState, ); + const setCommandMenuNavigationStack = useSetRecoilState( + commandMenuNavigationStackState, + ); + /** * This function is used in a context where dependencies shouldn't change much. * That's why its wrapped in a `useCallback` hook. Removing memoization might break the app unexpectedly. @@ -33,15 +41,22 @@ export const useStartNodeCreation = () => { }) => { setWorkflowInsertStepIds({ parentStepId, nextStepId, position }); - if (isDefined(workflowVisualizerWorkflowId)) { - openStepSelectInCommandMenu(workflowVisualizerWorkflowId); + if (!isDefined(workflowVisualizerWorkflowId)) { return; } + + if (!isInRightDrawer) { + setCommandMenuNavigationStack([]); + } + + openStepSelectInCommandMenu(workflowVisualizerWorkflowId); }, [ setWorkflowInsertStepIds, workflowVisualizerWorkflowId, + isInRightDrawer, openStepSelectInCommandMenu, + setCommandMenuNavigationStack, ], ); diff --git a/packages/twenty-front/src/modules/workflow/workflow-diagram/hooks/useTriggerNodeSelection.ts b/packages/twenty-front/src/modules/workflow/workflow-diagram/hooks/useTriggerNodeSelection.ts deleted file mode 100644 index 2574829d29..0000000000 --- a/packages/twenty-front/src/modules/workflow/workflow-diagram/hooks/useTriggerNodeSelection.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { useRecoilComponentState } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentState'; -import { workflowDiagramTriggerNodeSelectionComponentState } from '@/workflow/workflow-diagram/states/workflowDiagramTriggerNodeSelectionComponentState'; -import { - WorkflowDiagramEdge, - WorkflowDiagramNode, -} from '@/workflow/workflow-diagram/types/WorkflowDiagram'; -import { useReactFlow } from '@xyflow/react'; -import { useEffect } from 'react'; -import { isDefined } from 'twenty-shared/utils'; - -export const useTriggerNodeSelection = () => { - const reactflow = useReactFlow(); - - const [ - workflowDiagramTriggerNodeSelection, - setWorkflowDiagramTriggerNodeSelection, - ] = useRecoilComponentState( - workflowDiagramTriggerNodeSelectionComponentState, - ); - - useEffect(() => { - if (!isDefined(workflowDiagramTriggerNodeSelection)) { - return; - } - - reactflow.setNodes((nodes) => - nodes.map((node) => ({ - ...node, - selected: workflowDiagramTriggerNodeSelection === node.id, - })), - ); - - setWorkflowDiagramTriggerNodeSelection(undefined); - }, [ - reactflow, - setWorkflowDiagramTriggerNodeSelection, - workflowDiagramTriggerNodeSelection, - ]); -}; diff --git a/packages/twenty-front/src/modules/workflow/workflow-diagram/states/workflowDiagramTriggerNodeSelectionComponentState.ts b/packages/twenty-front/src/modules/workflow/workflow-diagram/states/workflowDiagramTriggerNodeSelectionComponentState.ts deleted file mode 100644 index d99710a29a..0000000000 --- a/packages/twenty-front/src/modules/workflow/workflow-diagram/states/workflowDiagramTriggerNodeSelectionComponentState.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { createComponentState } from '@/ui/utilities/state/component-state/utils/createComponentState'; -import { WorkflowVisualizerComponentInstanceContext } from '@/workflow/workflow-diagram/states/contexts/WorkflowVisualizerComponentInstanceContext'; - -export const workflowDiagramTriggerNodeSelectionComponentState = - createComponentState({ - key: 'workflowDiagramTriggerNodeSelectionComponentState', - defaultValue: undefined, - componentInstanceContext: WorkflowVisualizerComponentInstanceContext, - }); diff --git a/packages/twenty-front/src/modules/workflow/workflow-diagram/states/workflowRunDiagramAutomaticallyOpenedStepsComponentState.ts b/packages/twenty-front/src/modules/workflow/workflow-diagram/states/workflowRunDiagramAutomaticallyOpenedStepsComponentState.ts index 1851b1f029..9cf49e83bf 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-diagram/states/workflowRunDiagramAutomaticallyOpenedStepsComponentState.ts +++ b/packages/twenty-front/src/modules/workflow/workflow-diagram/states/workflowRunDiagramAutomaticallyOpenedStepsComponentState.ts @@ -2,7 +2,7 @@ import { createComponentState } from '@/ui/utilities/state/component-state/utils import { WorkflowVisualizerComponentInstanceContext } from '@/workflow/workflow-diagram/states/contexts/WorkflowVisualizerComponentInstanceContext'; export const workflowRunDiagramAutomaticallyOpenedStepsComponentState = - createComponentState({ + createComponentState<{ stepId: string; isInRightDrawer: boolean }[]>({ key: 'workflowRunDiagramAutomaticallyOpenedStepsComponentState', defaultValue: [], componentInstanceContext: WorkflowVisualizerComponentInstanceContext, diff --git a/packages/twenty-front/src/modules/workflow/workflow-diagram/states/workflowRunStepToOpenByDefaultComponentState.ts b/packages/twenty-front/src/modules/workflow/workflow-diagram/states/workflowRunStepToOpenByDefaultComponentState.ts deleted file mode 100644 index 60e560f909..0000000000 --- a/packages/twenty-front/src/modules/workflow/workflow-diagram/states/workflowRunStepToOpenByDefaultComponentState.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { createComponentState } from '@/ui/utilities/state/component-state/utils/createComponentState'; -import { WorkflowVisualizerComponentInstanceContext } from '@/workflow/workflow-diagram/states/contexts/WorkflowVisualizerComponentInstanceContext'; -import { WorkflowRunDiagramStepNodeData } from '@/workflow/workflow-diagram/types/WorkflowDiagram'; - -export const workflowRunStepToOpenByDefaultComponentState = - createComponentState< - | { - id: string; - data: WorkflowRunDiagramStepNodeData; - } - | undefined - >({ - key: 'workflowRunStepToOpenByDefaultComponentState', - defaultValue: undefined, - componentInstanceContext: WorkflowVisualizerComponentInstanceContext, - }); diff --git a/packages/twenty-front/src/modules/workflow/workflow-steps/hooks/useDeleteStep.ts b/packages/twenty-front/src/modules/workflow/workflow-steps/hooks/useDeleteStep.ts index 6c70c319f1..c1e7e85683 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-steps/hooks/useDeleteStep.ts +++ b/packages/twenty-front/src/modules/workflow/workflow-steps/hooks/useDeleteStep.ts @@ -30,8 +30,6 @@ export const useDeleteStep = ({ const deleteStep = async (stepId: string) => { assertWorkflowWithCurrentVersionIsDefined(workflow); - closeCommandMenu(); - const workflowVersionId = await getUpdatableWorkflowVersion(workflow); if (!isDefined(workflowVersionId)) { @@ -51,6 +49,9 @@ export const useDeleteStep = ({ stepId, }); } + + closeCommandMenu(); + deleteStepOutputSchema({ stepId, workflowVersionId, diff --git a/packages/twenty-front/src/modules/workflow/workflow-variables/hooks/useVariableDropdown.ts b/packages/twenty-front/src/modules/workflow/workflow-variables/hooks/useVariableDropdown.ts index 57ea4ab78a..2c8eed8874 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-variables/hooks/useVariableDropdown.ts +++ b/packages/twenty-front/src/modules/workflow/workflow-variables/hooks/useVariableDropdown.ts @@ -1,10 +1,16 @@ +import { useWorkflowCommandMenu } from '@/command-menu/hooks/useWorkflowCommandMenu'; +import { commandMenuNavigationStackState } from '@/command-menu/states/commandMenuNavigationStackState'; import { activeTabIdComponentState } from '@/ui/layout/tab-list/states/activeTabIdComponentState'; +import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue'; import { useSetRecoilComponentState } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentState'; -import { workflowDiagramTriggerNodeSelectionComponentState } from '@/workflow/workflow-diagram/states/workflowDiagramTriggerNodeSelectionComponentState'; +import { workflowVisualizerWorkflowIdComponentState } from '@/workflow/states/workflowVisualizerWorkflowIdComponentState'; +import { workflowDiagramComponentState } from '@/workflow/workflow-diagram/states/workflowDiagramComponentState'; import { workflowSelectedNodeComponentState } from '@/workflow/workflow-diagram/states/workflowSelectedNodeComponentState'; import { getVariableTemplateFromPath } from '@/workflow/workflow-variables/utils/getVariableTemplateFromPath'; import { useState } from 'react'; +import { useSetRecoilState } from 'recoil'; import { isDefined } from 'twenty-shared/utils'; +import { useIcons } from 'twenty-ui/display'; import { BaseOutputSchema, LinkOutputSchema, @@ -35,9 +41,17 @@ export const useVariableDropdown = ({ onSelect, onBack, }: UseVariableDropdownProps): UseVariableDropdownReturn => { + const { getIcon } = useIcons(); + const [currentPath, setCurrentPath] = useState([]); const [searchInputValue, setSearchInputValue] = useState(''); + const { openWorkflowEditStepInCommandMenu } = useWorkflowCommandMenu(); + + const workflowVisualizerWorkflowId = useRecoilComponentValue( + workflowVisualizerWorkflowIdComponentState, + ); + const setWorkflowSelectedNode = useSetRecoilComponentState( workflowSelectedNodeComponentState, ); @@ -45,8 +59,11 @@ export const useVariableDropdown = ({ activeTabIdComponentState, 'workflow-serverless-function-tab-list-component-id', ); - const setWorkflowDiagramTriggerNodeSelection = useSetRecoilComponentState( - workflowDiagramTriggerNodeSelectionComponentState, + const setWorkflowDiagram = useSetRecoilComponentState( + workflowDiagramComponentState, + ); + const setCommandMenuNavigationStack = useSetRecoilState( + commandMenuNavigationStackState, ); const getDisplayedSubStepFields = () => { @@ -83,8 +100,34 @@ export const useVariableDropdown = ({ const handleSelectLinkOutputSchema = ( linkOutputSchema: LinkOutputSchema, ) => { + if (!isDefined(workflowVisualizerWorkflowId)) { + throw new Error('Workflow ID must be configured'); + } + setWorkflowSelectedNode(step.id); - setWorkflowDiagramTriggerNodeSelection(step.id); + + setWorkflowDiagram((diagram) => { + if (!isDefined(diagram)) { + throw new Error('Workflow diagram must be defined'); + } + + return { + ...diagram, + nodes: diagram.nodes.map((node) => ({ + ...node, + selected: node.id === step.id, + })), + }; + }); + + setCommandMenuNavigationStack([]); + + openWorkflowEditStepInCommandMenu( + workflowVisualizerWorkflowId, + step.name, + getIcon(step.icon), + ); + if (isDefined(linkOutputSchema.link.tab)) { setActiveTabId(linkOutputSchema.link.tab); }