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
This commit is contained in:
Baptiste Devessier
2025-08-06 19:00:48 +02:00
committed by GitHub
parent 787f8b75ab
commit b8cd4491c2
31 changed files with 564 additions and 680 deletions
@@ -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 (
@@ -112,7 +112,13 @@ export const useRunWorkflowRunOpeningInCommandMenuSideEffects = () => {
recordId,
}),
}),
(steps) => [...steps, stepToOpenByDefault.id],
(steps) => [
...steps,
{
stepId: stepToOpenByDefault.id,
isInRightDrawer: true,
},
],
);
openWorkflowRunViewStepInCommandMenu({
workflowId: workflowRunRecord.workflowId,
@@ -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 = ({
<WorkflowDiagramCanvasBase
nodeTypes={{
default: WorkflowDiagramStepNodeEditable,
'empty-trigger': WorkflowDiagramEmptyTrigger,
'empty-trigger': WorkflowDiagramEmptyTriggerEditable,
}}
edgeTypes={{
blank: WorkflowDiagramBlankEdge,
@@ -1,103 +1,17 @@
import { useCallback, useContext } from 'react';
import { useSetRecoilState } from 'recoil';
import { ActionMenuContext } from '@/action-menu/contexts/ActionMenuContext';
import { commandMenuNavigationStackState } from '@/command-menu/states/commandMenuNavigationStackState';
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 { EMPTY_TRIGGER_STEP_ID } from '@/workflow/workflow-diagram/constants/EmptyTriggerStepId';
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 { isDefined } from 'twenty-shared/utils';
import { useIcons } from 'twenty-ui/display';
import { useEdgeSelected } from '@/workflow/workflow-diagram/hooks/useEdgeSelected';
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
import { OnSelectionChangeParams, useOnSelectionChange } from '@xyflow/react';
import { useCallback } from 'react';
import { isDefined } from 'twenty-shared/utils';
import { FeatureFlagKey } from '~/generated/graphql';
export const WorkflowDiagramCanvasEditableEffect = () => {
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;
};
@@ -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 = ({
<WorkflowDiagramCanvasBase
nodeTypes={{
default: WorkflowDiagramStepNodeReadonly,
'empty-trigger': WorkflowDiagramEmptyTrigger,
'empty-trigger': WorkflowDiagramEmptyTriggerReadonly,
}}
edgeTypes={{
'filtering-disabled--readonly':
@@ -35,8 +34,6 @@ export const WorkflowDiagramCanvasReadonly = ({
tagColor={tagProps.color}
tagText={tagProps.text}
/>
<WorkflowDiagramCanvasReadonlyEffect />
</ReactFlowProvider>
);
};
@@ -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;
};
@@ -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<WorkflowDiagramEdge>;
@@ -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,
@@ -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 (
<WorkflowDiagramStepNodeBase
id={'trigger'}
name="Add a Trigger"
nodeType="trigger"
variant="empty"
Icon={<StyledStepNodeLabelIconContainer />}
/>
);
};
@@ -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 (
<WorkflowDiagramStepNodeBase
id="trigger"
name={t`Add a Trigger`}
nodeType="trigger"
variant="empty"
Icon={<StyledStepNodeLabelIconContainer />}
onClick={() => {
if (!isInRightDrawer) {
setCommandMenuNavigationStack([]);
}
if (!isDefined(workflowVisualizerWorkflowId)) {
return;
}
openWorkflowTriggerTypeInCommandMenu(workflowVisualizerWorkflowId);
}}
/>
);
};
@@ -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 (
<WorkflowDiagramStepNodeBase
id="trigger"
name={t`Add a Trigger`}
nodeType="trigger"
variant="empty"
Icon={<StyledStepNodeLabelIconContainer />}
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),
});
}}
/>
);
};
@@ -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<WorkflowDiagramEdge>;
@@ -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,
@@ -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,
@@ -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<HTMLDivElement>) => void;
onMouseLeave?: (event: MouseEvent<HTMLDivElement>) => void;
}) => {
@@ -172,6 +174,7 @@ export const WorkflowDiagramStepNodeBase = ({
<StyledStepNodeContainer
className="workflow-node-container"
data-click-outside-id={WORKFLOW_DIAGRAM_STEP_NODE_BASE_CLICK_OUTSIDE_ID}
onClick={onClick}
onMouseEnter={onMouseEnter}
onMouseLeave={onMouseLeave}
>
@@ -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 (
<WorkflowDiagramStepNodeEditableContent
id={id}
data={data}
variant="default"
selected={selected ?? false}
onClick={() => {
if (!isInRightDrawer) {
setCommandMenuNavigationStack([]);
}
setWorkflowSelectedNode(id);
if (isDefined(workflowVisualizerWorkflowId)) {
openWorkflowEditStepInCommandMenu(
workflowVisualizerWorkflowId,
data.name,
getIcon(getWorkflowNodeIconKey(data)),
);
return;
}
}}
onDelete={() => {
deleteStep(id);
}}
@@ -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 = ({
<WorkflowDiagramStepNodeBase
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
onClick={onClick}
id={id}
name={data.name}
variant={variant}
@@ -1,8 +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 { workflowVisualizerWorkflowIdComponentState } from '@/workflow/states/workflowVisualizerWorkflowIdComponentState';
import { workflowVisualizerWorkflowVersionIdComponentState } from '@/workflow/states/workflowVisualizerWorkflowVersionIdComponentState';
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 { WorkflowDiagramStepNodeData } from '@/workflow/workflow-diagram/types/WorkflowDiagram';
import { getNodeVariantFromStepRunStatus } from '@/workflow/workflow-diagram/utils/getNodeVariantFromStepRunStatus';
import React from 'react';
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 WorkflowDiagramStepNodeReadonly = ({
id,
@@ -11,6 +23,27 @@ export const WorkflowDiagramStepNodeReadonly = ({
id: string;
data: WorkflowDiagramStepNodeData;
}) => {
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 (
<WorkflowDiagramStepNodeBase
id={id}
@@ -19,6 +52,27 @@ export const WorkflowDiagramStepNodeReadonly = ({
nodeType={data.nodeType}
Icon={<WorkflowDiagramStepNodeIcon data={data} />}
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)),
});
}}
/>
);
};
@@ -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 (
<ReactFlowProvider>
<WorkflowDiagramCanvasBase
nodeTypes={{
default: WorkflowDiagramStepNodeReadonly,
default: WorkflowRunDiagramStepNode,
}}
edgeTypes={{
'filtering-disabled--run': WorkflowDiagramFilteringDisabledEdgeRun,
@@ -35,10 +30,7 @@ export const WorkflowRunDiagramCanvas = ({
tagContainerTestId="workflow-run-status"
tagColor={tagProps.color}
tagText={tagProps.text}
onInit={handleWorkflowRunDiagramCanvasInit}
/>
<WorkflowRunDiagramCanvasEffect />
</ReactFlowProvider>
);
};
@@ -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;
};
@@ -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 (
<WorkflowDiagramStepNodeBase
id={id}
name={data.name}
variant={getNodeVariantFromStepRunStatus(data.runStatus)}
nodeType={data.nodeType}
Icon={<WorkflowDiagramStepNodeIcon data={data} />}
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,
});
}}
/>
);
};
@@ -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,
],
@@ -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<typeof WorkflowDiagramEmptyTrigger> = {
title: 'Modules/Workflow/WorkflowDiagramEmptyTrigger',
component: WorkflowDiagramEmptyTrigger,
const meta: Meta<typeof WorkflowDiagramEmptyTriggerEditable> = {
title: 'Modules/Workflow/WorkflowDiagramEmptyTriggerEditable',
component: WorkflowDiagramEmptyTriggerEditable,
args: {
data: {
nodeType: 'empty-trigger',
},
},
decorators: [I18nFrontDecorator],
};
export default meta;
type Story = StoryObj<typeof WorkflowDiagramEmptyTrigger>;
type Story = StoryObj<typeof WorkflowDiagramEmptyTriggerEditable>;
export const Default: Story = {
decorators: [
@@ -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;
@@ -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 }) => (
<RecoilRoot>
<WorkflowVisualizerComponentInstanceContext.Provider
value={{
instanceId: 'test-instance-id',
}}
>
{children}
</WorkflowVisualizerComponentInstanceContext.Provider>
</RecoilRoot>
);
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();
});
});
@@ -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,
};
};
@@ -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,
],
);
@@ -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<WorkflowDiagramNode, WorkflowDiagramEdge>();
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,
]);
};
@@ -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<string | undefined>({
key: 'workflowDiagramTriggerNodeSelectionComponentState',
defaultValue: undefined,
componentInstanceContext: WorkflowVisualizerComponentInstanceContext,
});
@@ -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<string[]>({
createComponentState<{ stepId: string; isInRightDrawer: boolean }[]>({
key: 'workflowRunDiagramAutomaticallyOpenedStepsComponentState',
defaultValue: [],
componentInstanceContext: WorkflowVisualizerComponentInstanceContext,
@@ -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,
});
@@ -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,
@@ -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<string[]>([]);
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);
}