Fix connection design (#14070)

- refactor edges related files into sub-module
- remove top handle from workflow, workflowVersion and workflowRun
- add a custom design for connection (edge being created)
- connect when hovering a connectable node
- fix marker z-index
- refactor Handles

## Before


https://github.com/user-attachments/assets/013dec36-5cc7-4561-938c-404511118578

<img width="934" height="409" alt="image"
src="https://github.com/user-attachments/assets/2b43c6cb-0bf3-4110-b1cc-b2e50c15c9a8"
/>



## After


https://github.com/user-attachments/assets/a5b32cbf-bfbd-48e6-9889-037234adf38f

<img width="1433" height="599" alt="image"
src="https://github.com/user-attachments/assets/02edc085-9b23-4434-b666-bf25e5c430a8"
/>
This commit is contained in:
martmull
2025-08-26 16:33:39 +02:00
committed by GitHub
parent 750dfcfc3b
commit 0f347e2f42
42 changed files with 359 additions and 323 deletions
@@ -10,7 +10,7 @@ import { PAGE_HEADER_COMMAND_MENU_BUTTON_CLICK_OUTSIDE_ID } from '@/ui/layout/pa
import { currentFocusIdSelector } from '@/ui/utilities/focus/states/currentFocusIdSelector';
import { useListenClickOutside } from '@/ui/utilities/pointer-event/hooks/useListenClickOutside';
import { WORKFLOW_DIAGRAM_CREATE_STEP_NODE_CLICK_OUTSIDE_ID } from '@/workflow/workflow-diagram/constants/WorkflowDiagramCreateStepNodeClickOutsideId';
import { WORKFLOW_DIAGRAM_EDGE_OPTIONS_CLICK_OUTSIDE_ID } from '@/workflow/workflow-diagram/constants/WorkflowDiagramEdgeOptionsClickOutsideId';
import { WORKFLOW_DIAGRAM_EDGE_OPTIONS_CLICK_OUTSIDE_ID } from '@/workflow/workflow-diagram/workflow-edges/constants/WorkflowDiagramEdgeOptionsClickOutsideId';
import { WORKFLOW_DIAGRAM_STEP_NODE_BASE_CLICK_OUTSIDE_ID } from '@/workflow/workflow-diagram/constants/WorkflowDiagramStepNodeClickOutsideId';
import { useTheme } from '@emotion/react';
@@ -5,7 +5,7 @@ import { useRecoilComponentCallbackState } from '@/ui/utilities/state/component-
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
import { useSetRecoilComponentState } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentState';
import { getSnapshotValue } from '@/ui/utilities/state/utils/getSnapshotValue';
import { WorkflowDiagramCustomMarkers } from '@/workflow/workflow-diagram/components/WorkflowDiagramCustomMarkers';
import { WorkflowDiagramCustomMarkers } from '@/workflow/workflow-diagram/workflow-edges/components/WorkflowDiagramCustomMarkers';
import { WorkflowDiagramRightClickCommandMenu } from '@/workflow/workflow-diagram/components/WorkflowDiagramRightClickCommandMenu';
import { useRightDrawerState } from '@/workflow/workflow-diagram/hooks/useRightDrawerState';
import { workflowDiagramComponentState } from '@/workflow/workflow-diagram/states/workflowDiagramComponentState';
@@ -54,7 +54,8 @@ import { isDefined } from 'twenty-shared/utils';
import { Tag, type TagColor } from 'twenty-ui/components';
import { THEME_COMMON } from 'twenty-ui/theme';
import { FeatureFlagKey } from '~/generated/graphql';
import { useEdgeState } from '@/workflow/workflow-diagram/hooks/useEdgeState';
import { useEdgeState } from '@/workflow/workflow-diagram/workflow-edges/hooks/useEdgeState';
import { WorkflowDiagramConnection } from '@/workflow/workflow-diagram/workflow-edges/components/WorkflowDiagramConnection';
const StyledResetReactflowStyles = styled.div`
height: 100%;
@@ -496,6 +497,8 @@ export const WorkflowDiagramCanvasBase = ({
nodesConnectable={isWorkflowBranchEnabled ? nodesConnectable : false}
paneClickDistance={10} // Fix small unwanted user dragging does not select node
preventScrolling={false}
connectionLineComponent={WorkflowDiagramConnection}
connectionRadius={0}
>
<Background color={theme.border.color.medium} size={2} />
@@ -2,11 +2,10 @@ 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 { WorkflowDiagramBlankEdge } from '@/workflow/workflow-diagram/components/WorkflowDiagramBlankEdge';
import { WorkflowDiagramBlankEdge } from '@/workflow/workflow-diagram/workflow-edges/components/WorkflowDiagramBlankEdge';
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 { WorkflowDiagramDefaultEdgeEditable } from '@/workflow/workflow-diagram/workflow-edges/components/WorkflowDiagramDefaultEdgeEditable';
import { workflowDiagramComponentState } from '@/workflow/workflow-diagram/states/workflowDiagramComponentState';
import { workflowDiagramRightClickMenuPositionState } from '@/workflow/workflow-diagram/states/workflowDiagramRightClickMenuPositionState';
import {
@@ -15,7 +14,7 @@ import {
} from '@/workflow/workflow-diagram/types/WorkflowDiagram';
import { getWorkflowVersionStatusTagProps } from '@/workflow/workflow-diagram/utils/getWorkflowVersionStatusTagProps';
import { WorkflowDiagramEmptyTriggerEditable } from '@/workflow/workflow-diagram/workflow-nodes/components/WorkflowDiagramEmptyTriggerEditable';
import { WorkflowDiagramFilterEdgeEditable } from '@/workflow/workflow-diagram/workflow-nodes/components/WorkflowDiagramFilterEdgeEditable';
import { WorkflowDiagramFilterEdgeEditable } from '@/workflow/workflow-diagram/workflow-edges/components/WorkflowDiagramFilterEdgeEditable';
import { WorkflowDiagramStepNodeEditable } from '@/workflow/workflow-diagram/workflow-nodes/components/WorkflowDiagramStepNodeEditable';
import { useCreateEdge } from '@/workflow/workflow-steps/hooks/useCreateEdge';
import { useDeleteEdge } from '@/workflow/workflow-steps/hooks/useDeleteEdge';
@@ -1,4 +1,4 @@
import { useEdgeState } from '@/workflow/workflow-diagram/hooks/useEdgeState';
import { useEdgeState } from '@/workflow/workflow-diagram/workflow-edges/hooks/useEdgeState';
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
import {
type OnSelectionChangeParams,
@@ -1,10 +1,10 @@
import { type WorkflowVersionStatus } from '@/workflow/types/Workflow';
import { WorkflowDiagramCanvasBase } from '@/workflow/workflow-diagram/components/WorkflowDiagramCanvasBase';
import { WorkflowDiagramDefaultEdgeReadonly } from '@/workflow/workflow-diagram/components/WorkflowDiagramDefaultEdgeReadonly';
import { WorkflowDiagramDefaultEdgeReadonly } from '@/workflow/workflow-diagram/workflow-edges/components/WorkflowDiagramDefaultEdgeReadonly';
import { getWorkflowVersionStatusTagProps } from '@/workflow/workflow-diagram/utils/getWorkflowVersionStatusTagProps';
import { WorkflowDiagramEmptyTriggerReadonly } from '@/workflow/workflow-diagram/workflow-nodes/components/WorkflowDiagramEmptyTriggerReadonly';
import { WorkflowDiagramFilterEdgeReadonly } from '@/workflow/workflow-diagram/workflow-nodes/components/WorkflowDiagramFilterEdgeReadonly';
import { WorkflowDiagramFilterEdgeReadonly } from '@/workflow/workflow-diagram/workflow-edges/components/WorkflowDiagramFilterEdgeReadonly';
import { WorkflowDiagramStepNodeReadonly } from '@/workflow/workflow-diagram/workflow-nodes/components/WorkflowDiagramStepNodeReadonly';
import { ReactFlowProvider } from '@xyflow/react';
@@ -1,7 +1,7 @@
import { type WorkflowRunStatus } from '@/workflow/types/Workflow';
import { WorkflowDiagramCanvasBase } from '@/workflow/workflow-diagram/components/WorkflowDiagramCanvasBase';
import { WorkflowDiagramDefaultEdgeRun } from '@/workflow/workflow-diagram/components/WorkflowDiagramDefaultEdgeRun';
import { WorkflowDiagramFilterEdgeRun } from '@/workflow/workflow-diagram/components/WorkflowDiagramFilterEdgeRun';
import { WorkflowDiagramDefaultEdgeRun } from '@/workflow/workflow-diagram/workflow-edges/components/WorkflowDiagramDefaultEdgeRun';
import { WorkflowDiagramFilterEdgeRun } from '@/workflow/workflow-diagram/workflow-edges/components/WorkflowDiagramFilterEdgeRun';
import { getWorkflowRunStatusTagProps } from '@/workflow/workflow-diagram/utils/getWorkflowRunStatusTagProps';
import { WorkflowRunDiagramStepNode } from '@/workflow/workflow-diagram/workflow-nodes/components/WorkflowRunDiagramStepNode';
@@ -1,5 +0,0 @@
export enum EdgeBranchArrowMarker {
Default = 'edge-branch-arrow-default',
Hover = 'edge-branch-arrow-hover',
Selected = 'edge-branch-arrow-selected',
}
@@ -1,132 +0,0 @@
import { workflowSelectedEdgeComponentState } from '@/workflow/workflow-diagram/states/workflowSelectedEdgeComponentState';
import { useRecoilComponentState } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentState';
import { type WorkflowDiagramNodeHandles } from '@/workflow/workflow-diagram/types/WorkflowDiagramNodeHandles';
import { type WorkflowDiagramEdge } from '@/workflow/workflow-diagram/types/WorkflowDiagramEdge';
import { workflowHoveredEdgeComponentState } from '@/workflow/workflow-diagram/states/workflowHoveredEdgeComponentState';
import { useReactFlow } from '@xyflow/react';
import { EdgeBranchArrowMarker } from '@/workflow/workflow-diagram/constants/EdgeBranchArrowMarker';
export const useEdgeState = () => {
const reactflow = useReactFlow();
const [workflowSelectedEdge, setWorkflowSelectedEdge] =
useRecoilComponentState(workflowSelectedEdgeComponentState);
const [workflowHoveredEdge, setWorkflowHoveredEdge] = useRecoilComponentState(
workflowHoveredEdgeComponentState,
);
const isEdgeSelected = ({ source, target }: WorkflowDiagramEdge) => {
return (
workflowSelectedEdge?.source === source &&
workflowSelectedEdge?.target === target
);
};
const isEdgeHovered = ({ source, target }: WorkflowDiagramEdge) => {
return (
workflowHoveredEdge?.source === source &&
workflowHoveredEdge?.target === target
);
};
const getNodeHandlesSelectedState = (
id: string,
): WorkflowDiagramNodeHandles => {
return {
targetHandle: workflowSelectedEdge?.target === id,
sourceHandle: workflowSelectedEdge?.source === id,
};
};
const getNodeHandlesHoveredState = (
id: string,
): WorkflowDiagramNodeHandles => {
return {
targetHandle: workflowHoveredEdge?.target === id,
sourceHandle: workflowHoveredEdge?.source === id,
};
};
const setEdgeSelected = ({ source, target }: WorkflowDiagramEdge) => {
if (
workflowSelectedEdge?.source === source &&
workflowSelectedEdge?.target === target
) {
return;
}
setWorkflowSelectedEdge({ source, target });
reactflow.setEdges((edges) =>
edges.map((edge) => ({
...edge,
markerEnd:
edge.source === source && edge.target === target
? EdgeBranchArrowMarker.Selected
: EdgeBranchArrowMarker.Default,
})),
);
};
const setEdgeHovered = ({ source, target }: WorkflowDiagramEdge) => {
if (
workflowHoveredEdge?.source === source &&
workflowHoveredEdge?.target === target
) {
return;
}
setWorkflowHoveredEdge({ source, target });
reactflow.setEdges((edges) =>
edges.map((edge) =>
edge.source === source &&
edge.target === target &&
edge.markerEnd !== EdgeBranchArrowMarker.Selected
? {
...edge,
markerEnd: EdgeBranchArrowMarker.Hover,
}
: edge,
),
);
};
const clearEdgeSelected = () => {
setWorkflowSelectedEdge(undefined);
reactflow.setEdges((edges) =>
edges.map((edge) => ({
...edge,
markerEnd: EdgeBranchArrowMarker.Default,
})),
);
};
const clearEdgeHover = () => {
setWorkflowHoveredEdge(undefined);
reactflow.setEdges((edges) =>
edges.map((edge) =>
edge.markerEnd === EdgeBranchArrowMarker.Hover
? {
...edge,
markerEnd: EdgeBranchArrowMarker.Default,
}
: edge,
),
);
};
return {
isEdgeSelected,
setEdgeSelected,
clearEdgeSelected,
getNodeHandlesSelectedState,
isEdgeHovered,
setEdgeHovered,
clearEdgeHover,
getNodeHandlesHoveredState,
};
};
@@ -125,6 +125,7 @@ describe('generateWorkflowRunDiagram', () => {
"source": "trigger",
"target": "step1",
"type": "empty-filter--run",
"zIndex": -2,
},
{
"data": {
@@ -139,6 +140,7 @@ describe('generateWorkflowRunDiagram', () => {
"source": "step1",
"target": "step2",
"type": "empty-filter--run",
"zIndex": -2,
},
{
"data": {
@@ -153,6 +155,7 @@ describe('generateWorkflowRunDiagram', () => {
"source": "step2",
"target": "step3",
"type": "empty-filter--run",
"zIndex": -2,
},
],
"nodes": [
@@ -355,6 +358,7 @@ describe('generateWorkflowRunDiagram', () => {
"source": "trigger",
"target": "step1",
"type": "empty-filter--run",
"zIndex": -2,
},
{
"data": {
@@ -369,6 +373,7 @@ describe('generateWorkflowRunDiagram', () => {
"source": "step1",
"target": "step2",
"type": "empty-filter--run",
"zIndex": -2,
},
{
"data": {
@@ -383,6 +388,7 @@ describe('generateWorkflowRunDiagram', () => {
"source": "step2",
"target": "step3",
"type": "empty-filter--run",
"zIndex": -2,
},
],
"nodes": [
@@ -585,6 +591,7 @@ describe('generateWorkflowRunDiagram', () => {
"source": "trigger",
"target": "step1",
"type": "empty-filter--run",
"zIndex": -2,
},
{
"data": {
@@ -599,6 +606,7 @@ describe('generateWorkflowRunDiagram', () => {
"source": "step1",
"target": "step2",
"type": "empty-filter--run",
"zIndex": -2,
},
{
"data": {
@@ -613,6 +621,7 @@ describe('generateWorkflowRunDiagram', () => {
"source": "step2",
"target": "step3",
"type": "empty-filter--run",
"zIndex": -2,
},
],
"nodes": [
@@ -834,6 +843,7 @@ describe('generateWorkflowRunDiagram', () => {
"source": "trigger",
"target": "step1",
"type": "empty-filter--run",
"zIndex": -2,
},
{
"data": {
@@ -848,6 +858,7 @@ describe('generateWorkflowRunDiagram', () => {
"source": "step1",
"target": "step2",
"type": "empty-filter--run",
"zIndex": -2,
},
{
"data": {
@@ -862,6 +873,7 @@ describe('generateWorkflowRunDiagram', () => {
"source": "step2",
"target": "step3",
"type": "empty-filter--run",
"zIndex": -2,
},
{
"data": {
@@ -876,6 +888,7 @@ describe('generateWorkflowRunDiagram', () => {
"source": "step3",
"target": "step4",
"type": "empty-filter--run",
"zIndex": -2,
},
],
"nodes": [
@@ -1063,6 +1076,7 @@ describe('generateWorkflowRunDiagram', () => {
"source": "trigger",
"target": "step1",
"type": "empty-filter--run",
"zIndex": -2,
},
],
"nodes": [
@@ -163,6 +163,7 @@ describe('getWorkflowVersionDiagram', () => {
"source": "trigger",
"target": "step-1",
"type": "empty-filter--editable",
"zIndex": -2,
},
],
"nodes": [
@@ -4,7 +4,7 @@ import {
} from '@/workflow/types/Workflow';
import { FIRST_NODE_POSITION } from '@/workflow/workflow-diagram/constants/FirstNodePosition';
import { VERTICAL_DISTANCE_BETWEEN_TWO_NODES } from '@/workflow/workflow-diagram/constants/VerticalDistanceBetweenTwoNodes';
import { WORKFLOW_VISUALIZER_EDGE_DEFAULT_CONFIGURATION } from '@/workflow/workflow-diagram/constants/WorkflowVisualizerEdgeDefaultConfiguration';
import { WORKFLOW_VISUALIZER_EDGE_DEFAULT_CONFIGURATION } from '@/workflow/workflow-diagram/workflow-edges/constants/WorkflowVisualizerEdgeDefaultConfiguration';
import {
type WorkflowDiagram,
type WorkflowDiagramEdge,
@@ -1,7 +1,7 @@
import { useTheme } from '@emotion/react';
import { BaseEdge, type EdgeProps } from '@xyflow/react';
import { type WorkflowDiagramEdge } from '@/workflow/workflow-diagram/types/WorkflowDiagram';
import { useEdgeState } from '@/workflow/workflow-diagram/hooks/useEdgeState';
import { useEdgeState } from '@/workflow/workflow-diagram/workflow-edges/hooks/useEdgeState';
type WorkflowDiagramBaseEdgeProps = Pick<
EdgeProps<WorkflowDiagramEdge>,
@@ -0,0 +1,33 @@
import {
type ConnectionLineComponentProps,
getBezierPath,
} from '@xyflow/react';
import { useTheme } from '@emotion/react';
import { EDGE_BRANCH_ARROW_MARKER } from '@/workflow/workflow-diagram/workflow-edges/constants/EdgeBranchArrowMarker';
type WorkflowDiagramConnectionProps = ConnectionLineComponentProps;
export const WorkflowDiagramConnection = ({
fromX,
fromY,
toX,
toY,
}: WorkflowDiagramConnectionProps) => {
const theme = useTheme();
const [path] = getBezierPath({
sourceX: fromX,
sourceY: fromY + 4,
targetX: toX,
targetY: toY - 4,
});
return (
<path
d={path}
fill="none"
stroke={theme.color.blue}
markerEnd={`url(#${EDGE_BRANCH_ARROW_MARKER.Selected.markerEnd})`}
/>
);
};
@@ -1,4 +1,4 @@
import { EdgeBranchArrowMarker } from '@/workflow/workflow-diagram/constants/EdgeBranchArrowMarker';
import { EDGE_BRANCH_ARROW_MARKER } from '@/workflow/workflow-diagram/workflow-edges/constants/EdgeBranchArrowMarker';
import { useTheme } from '@emotion/react';
export const WorkflowDiagramCustomMarkers = () => {
@@ -8,7 +8,7 @@ export const WorkflowDiagramCustomMarkers = () => {
<svg style={{ position: 'absolute', top: 0, left: 0 }}>
<defs>
<marker
id={EdgeBranchArrowMarker.Default}
id={EDGE_BRANCH_ARROW_MARKER.Default.markerEnd}
markerHeight={8}
markerWidth={10}
refX={5}
@@ -22,7 +22,7 @@ export const WorkflowDiagramCustomMarkers = () => {
</marker>
<marker
id={EdgeBranchArrowMarker.Hover}
id={EDGE_BRANCH_ARROW_MARKER.Hover.markerEnd}
markerHeight={8}
markerWidth={10}
refX={5}
@@ -36,7 +36,7 @@ export const WorkflowDiagramCustomMarkers = () => {
</marker>
<marker
id={EdgeBranchArrowMarker.Selected}
id={EDGE_BRANCH_ARROW_MARKER.Selected.markerEnd}
markerHeight={8}
markerWidth={10}
refX={5}
@@ -3,11 +3,11 @@ import { commandMenuNavigationStackState } from '@/command-menu/states/commandMe
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 { WorkflowDiagramBaseEdge } from '@/workflow/workflow-diagram/workflow-edges/components/WorkflowDiagramBaseEdge';
import { WorkflowDiagramEdgeButtonGroup } from '@/workflow/workflow-diagram/workflow-edges/components/WorkflowDiagramEdgeButtonGroup';
import { WorkflowDiagramEdgeV2Container } from '@/workflow/workflow-diagram/workflow-edges/components/WorkflowDiagramEdgeV2Container';
import { WorkflowDiagramEdgeV2VisibilityContainer } from '@/workflow/workflow-diagram/workflow-edges/components/WorkflowDiagramEdgeV2VisibilityContainer';
import { WORKFLOW_DIAGRAM_EDGE_OPTIONS_CLICK_OUTSIDE_ID } from '@/workflow/workflow-diagram/workflow-edges/constants/WorkflowDiagramEdgeOptionsClickOutsideId';
import { useOpenWorkflowEditFilterInCommandMenu } from '@/workflow/workflow-diagram/hooks/useOpenWorkflowEditFilterInCommandMenu';
import { useStartNodeCreation } from '@/workflow/workflow-diagram/hooks/useStartNodeCreation';
import { type WorkflowDiagramEdge } from '@/workflow/workflow-diagram/types/WorkflowDiagram';
@@ -24,7 +24,7 @@ import { useSetRecoilState } from 'recoil';
import { isDefined } from 'twenty-shared/utils';
import { IconFilter, IconPlus, IconTrash } from 'twenty-ui/display';
import { FeatureFlagKey } from '~/generated/graphql';
import { useEdgeState } from '@/workflow/workflow-diagram/hooks/useEdgeState';
import { useEdgeState } from '@/workflow/workflow-diagram/workflow-edges/hooks/useEdgeState';
type WorkflowDiagramDefaultEdgeEditableProps = EdgeProps<WorkflowDiagramEdge>;
@@ -1,4 +1,4 @@
import { WorkflowRunDiagramBaseEdge } from '@/workflow/workflow-diagram/components/WorkflowRunDiagramBaseEdge';
import { WorkflowRunDiagramBaseEdge } from '@/workflow/workflow-diagram/workflow-edges/components/WorkflowRunDiagramBaseEdge';
import { type WorkflowDiagramEdge } from '@/workflow/workflow-diagram/types/WorkflowDiagram';
import { type EdgeProps, getBezierPath } from '@xyflow/react';
@@ -9,11 +9,12 @@ import { useOpenDropdown } from '@/ui/layout/dropdown/hooks/useOpenDropdown';
import { isDropdownOpenComponentState } from '@/ui/layout/dropdown/states/isDropdownOpenComponentState';
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
import { useSetRecoilComponentState } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentState';
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 { WorkflowDiagramBaseEdge } from '@/workflow/workflow-diagram/workflow-edges/components/WorkflowDiagramBaseEdge';
import { WorkflowDiagramEdgeButtonGroup } from '@/workflow/workflow-diagram/workflow-edges/components/WorkflowDiagramEdgeButtonGroup';
import { WorkflowDiagramEdgeV2Container } from '@/workflow/workflow-diagram/workflow-edges/components/WorkflowDiagramEdgeV2Container';
import { WorkflowDiagramEdgeV2VisibilityContainer } from '@/workflow/workflow-diagram/workflow-edges/components/WorkflowDiagramEdgeV2VisibilityContainer';
import { useEdgeState } from '@/workflow/workflow-diagram/workflow-edges/hooks/useEdgeState';
import { WORKFLOW_DIAGRAM_EDGE_OPTIONS_CLICK_OUTSIDE_ID } from '@/workflow/workflow-diagram/workflow-edges/constants/WorkflowDiagramEdgeOptionsClickOutsideId';
import { useOpenWorkflowEditFilterInCommandMenu } from '@/workflow/workflow-diagram/hooks/useOpenWorkflowEditFilterInCommandMenu';
import { useStartNodeCreation } from '@/workflow/workflow-diagram/hooks/useStartNodeCreation';
import { workflowDiagramPanOnDragComponentState } from '@/workflow/workflow-diagram/states/workflowDiagramPanOnDragComponentState';
@@ -47,7 +48,6 @@ import {
import { IconButtonGroup } from 'twenty-ui/input';
import { MenuItem } from 'twenty-ui/navigation';
import { FeatureFlagKey } from '~/generated/graphql';
import { useEdgeState } from '@/workflow/workflow-diagram/hooks/useEdgeState';
type WorkflowDiagramFilterEdgeEditableProps = EdgeProps<WorkflowDiagramEdge>;
@@ -1,9 +1,9 @@
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';
import { WORKFLOW_DIAGRAM_EDGE_OPTIONS_CLICK_OUTSIDE_ID } from '@/workflow/workflow-diagram/constants/WorkflowDiagramEdgeOptionsClickOutsideId';
import { WorkflowDiagramEdgeV2Container } from '@/workflow/workflow-diagram/workflow-edges/components/WorkflowDiagramEdgeV2Container';
import { WorkflowDiagramEdgeV2VisibilityContainer } from '@/workflow/workflow-diagram/workflow-edges/components/WorkflowDiagramEdgeV2VisibilityContainer';
import { WORKFLOW_DIAGRAM_EDGE_OPTIONS_CLICK_OUTSIDE_ID } from '@/workflow/workflow-diagram/workflow-edges/constants/WorkflowDiagramEdgeOptionsClickOutsideId';
import { useOpenWorkflowViewFilterInCommandMenu } from '@/workflow/workflow-diagram/hooks/useOpenWorkflowViewFilterInCommandMenu';
import { workflowSelectedNodeComponentState } from '@/workflow/workflow-diagram/states/workflowSelectedNodeComponentState';
import {
@@ -1,8 +1,8 @@
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';
import { WorkflowRunDiagramBaseEdge } from '@/workflow/workflow-diagram/components/WorkflowRunDiagramBaseEdge';
import { WORKFLOW_DIAGRAM_EDGE_OPTIONS_CLICK_OUTSIDE_ID } from '@/workflow/workflow-diagram/constants/WorkflowDiagramEdgeOptionsClickOutsideId';
import { WorkflowDiagramEdgeV2Container } from '@/workflow/workflow-diagram/workflow-edges/components/WorkflowDiagramEdgeV2Container';
import { WorkflowDiagramEdgeV2VisibilityContainer } from '@/workflow/workflow-diagram/workflow-edges/components/WorkflowDiagramEdgeV2VisibilityContainer';
import { WorkflowRunDiagramBaseEdge } from '@/workflow/workflow-diagram/workflow-edges/components/WorkflowRunDiagramBaseEdge';
import { WORKFLOW_DIAGRAM_EDGE_OPTIONS_CLICK_OUTSIDE_ID } from '@/workflow/workflow-diagram/workflow-edges/constants/WorkflowDiagramEdgeOptionsClickOutsideId';
import { useOpenWorkflowRunFilterInCommandMenu } from '@/workflow/workflow-diagram/hooks/useOpenWorkflowRunFilterInCommandMenu';
import { workflowSelectedNodeComponentState } from '@/workflow/workflow-diagram/states/workflowSelectedNodeComponentState';
import {
@@ -0,0 +1,23 @@
enum EdgeBranchArrowState {
Default = 'Default',
Hover = 'Hover',
Selected = 'Selected',
}
export const EDGE_BRANCH_ARROW_MARKER: Record<
EdgeBranchArrowState,
{ markerEnd: string; zIndex: number }
> = {
[EdgeBranchArrowState.Default]: {
markerEnd: 'edge-branch-arrow-default',
zIndex: -2,
},
[EdgeBranchArrowState.Hover]: {
markerEnd: 'edge-branch-arrow-hover',
zIndex: -1,
},
[EdgeBranchArrowState.Selected]: {
markerEnd: 'edge-branch-arrow-selected',
zIndex: 0,
},
};
@@ -1,4 +1,4 @@
import { EdgeBranchArrowMarker } from '@/workflow/workflow-diagram/constants/EdgeBranchArrowMarker';
import { EDGE_BRANCH_ARROW_MARKER } from '@/workflow/workflow-diagram/workflow-edges/constants/EdgeBranchArrowMarker';
import {
type WorkflowDiagramEdge,
type WorkflowDiagramEdgeType,
@@ -7,7 +7,7 @@ import {
export const WORKFLOW_VISUALIZER_EDGE_DEFAULT_CONFIGURATION = {
type: 'empty-filter--readonly' satisfies WorkflowDiagramEdgeType,
markerStart: undefined,
markerEnd: EdgeBranchArrowMarker.Default,
...EDGE_BRANCH_ARROW_MARKER.Default,
deletable: false,
selectable: false,
data: {
@@ -0,0 +1,117 @@
import { workflowSelectedEdgeComponentState } from '@/workflow/workflow-diagram/workflow-edges/states/workflowSelectedEdgeComponentState';
import { useRecoilComponentState } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentState';
import { type WorkflowDiagramEdge } from '@/workflow/workflow-diagram/workflow-edges/types/WorkflowDiagramEdge';
import { workflowHoveredEdgeComponentState } from '@/workflow/workflow-diagram/workflow-edges/states/workflowHoveredEdgeComponentState';
import { useReactFlow } from '@xyflow/react';
import { EDGE_BRANCH_ARROW_MARKER } from '@/workflow/workflow-diagram/workflow-edges/constants/EdgeBranchArrowMarker';
export const useEdgeState = () => {
const reactflow = useReactFlow();
const [workflowSelectedEdge, setWorkflowSelectedEdge] =
useRecoilComponentState(workflowSelectedEdgeComponentState);
const [workflowHoveredEdge, setWorkflowHoveredEdge] = useRecoilComponentState(
workflowHoveredEdgeComponentState,
);
const isSourceSelected = (nodeId: string) => {
return workflowSelectedEdge?.source === nodeId;
};
const isSourceHovered = (nodeId: string) => {
return workflowHoveredEdge?.source === nodeId;
};
const isEdgeSelected = ({ source, target }: WorkflowDiagramEdge) => {
return isSourceSelected(source) && workflowSelectedEdge?.target === target;
};
const isEdgeHovered = ({ source, target }: WorkflowDiagramEdge) => {
return isSourceHovered(source) && workflowHoveredEdge?.target === target;
};
const setEdgeSelected = ({ source, target }: WorkflowDiagramEdge) => {
if (isEdgeSelected({ source, target })) {
return;
}
setWorkflowSelectedEdge({ source, target });
reactflow.setEdges((edges) =>
edges.map((edge) => {
if (edge.source === source && edge.target === target) {
return {
...edge,
...EDGE_BRANCH_ARROW_MARKER.Selected,
};
}
return {
...edge,
...EDGE_BRANCH_ARROW_MARKER.Default,
};
}),
);
};
const setEdgeHovered = ({ source, target }: WorkflowDiagramEdge) => {
setWorkflowHoveredEdge({ source, target });
reactflow.setEdges((edges) =>
edges.map((edge) => {
if (
edge.source === source &&
edge.target === target &&
edge.markerEnd !== EDGE_BRANCH_ARROW_MARKER.Selected.markerEnd
) {
return {
...edge,
...EDGE_BRANCH_ARROW_MARKER.Hover,
};
}
return edge;
}),
);
};
const clearEdgeSelected = () => {
setWorkflowSelectedEdge(undefined);
reactflow.setEdges((edges) =>
edges.map((edge) => ({
...edge,
...EDGE_BRANCH_ARROW_MARKER.Default,
})),
);
};
const clearEdgeHover = () => {
setWorkflowHoveredEdge(undefined);
reactflow.setEdges((edges) =>
edges.map((edge) => {
if (edge.markerEnd === EDGE_BRANCH_ARROW_MARKER.Hover.markerEnd) {
return {
...edge,
...EDGE_BRANCH_ARROW_MARKER.Default,
};
}
return edge;
}),
);
};
return {
isEdgeSelected,
isSourceSelected,
setEdgeSelected,
clearEdgeSelected,
isEdgeHovered,
isSourceHovered,
setEdgeHovered,
clearEdgeHover,
};
};
@@ -1,6 +1,6 @@
import { createComponentState } from '@/ui/utilities/state/component-state/utils/createComponentState';
import { WorkflowVisualizerComponentInstanceContext } from '@/workflow/workflow-diagram/states/contexts/WorkflowVisualizerComponentInstanceContext';
import type { WorkflowDiagramEdge } from '@/workflow/workflow-diagram/types/WorkflowDiagramEdge';
import { type WorkflowDiagramEdge } from '@/workflow/workflow-diagram/workflow-edges/types/WorkflowDiagramEdge';
export const workflowHoveredEdgeComponentState = createComponentState<
WorkflowDiagramEdge | undefined
@@ -1,6 +1,6 @@
import { createComponentState } from '@/ui/utilities/state/component-state/utils/createComponentState';
import { WorkflowVisualizerComponentInstanceContext } from '@/workflow/workflow-diagram/states/contexts/WorkflowVisualizerComponentInstanceContext';
import { type WorkflowDiagramEdge } from '@/workflow/workflow-diagram/types/WorkflowDiagramEdge';
import { type WorkflowDiagramEdge } from '@/workflow/workflow-diagram/workflow-edges/types/WorkflowDiagramEdge';
export const workflowSelectedEdgeComponentState = createComponentState<
WorkflowDiagramEdge | undefined
@@ -1,53 +0,0 @@
import { NODE_HANDLE_HEIGHT_PX } from '@/workflow/workflow-diagram/constants/NodeHandleHeightPx';
import { NODE_HANDLE_WIDTH_PX } from '@/workflow/workflow-diagram/constants/NodeHandleWidthPx';
import { css } from '@emotion/react';
import styled from '@emotion/styled';
import { Handle, type HandleProps } from '@xyflow/react';
type WorkflowDiagramHandleReadonlyProps = HandleProps & {
selected: boolean;
};
const StyledHandle = styled(Handle)<{
selected: boolean;
}>`
&.react-flow__handle {
height: ${NODE_HANDLE_HEIGHT_PX}px;
width: ${NODE_HANDLE_WIDTH_PX}px;
border-color: ${({ theme, selected }) =>
selected ? theme.color.blue : theme.border.color.strong};
background: ${({ theme, selected }) =>
selected ? theme.adaptiveColors.blue1 : theme.background.primary};
transition:
transform 0.1s ease-out,
background 0.1s,
border-color 0.1s;
z-index: 1;
${({ position }) => {
if (position === 'top') {
return css`
transform: translate(-50%, -50%);
transform-origin: top left;
`;
} else if (position === 'bottom') {
return css`
transform: translate(-50%, 50%);
transform-origin: bottom left;
`;
}
}}
&.connectionindicator {
cursor: default;
}
}
`;
export const WorkflowDiagramHandleReadonly = ({
type,
position,
selected,
}: WorkflowDiagramHandleReadonlyProps) => {
return <StyledHandle type={type} position={position} selected={selected} />;
};
@@ -3,12 +3,13 @@ import { NODE_HANDLE_WIDTH_PX } from '@/workflow/workflow-diagram/constants/Node
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
import { css } from '@emotion/react';
import styled from '@emotion/styled';
import { Handle, type HandleProps } from '@xyflow/react';
import { Handle, Position, type HandleProps } from '@xyflow/react';
import { FeatureFlagKey } from '~/generated/graphql';
type WorkflowDiagramHandleEditableProps = HandleProps & {
type WorkflowDiagramHandleSourceProps = {
selected: boolean;
hovered?: boolean;
readOnly?: boolean;
};
const HANDLE_SCALE_ON_HOVER = 1.5;
@@ -17,17 +18,19 @@ const StyledHandle = styled(Handle, {
shouldForwardProp: (prop) =>
prop !== 'disableHoverEffect' && prop !== 'selected' && prop !== 'hovered',
})<{
type: HandleProps['type'];
disableHoverEffect: boolean;
selected: boolean;
hovered?: boolean;
}>`
&.react-flow__handle {
opacity: ${({ type }) => (type === 'target' ? 0 : 1)};
height: ${NODE_HANDLE_HEIGHT_PX}px;
width: ${NODE_HANDLE_WIDTH_PX}px;
border-color: ${({ theme, selected, hovered }) =>
border-color: ${({ theme, selected, hovered, disableHoverEffect }) =>
selected
? theme.color.blue
: hovered
: hovered && !disableHoverEffect
? theme.font.color.light
: theme.border.color.strong};
background: ${({ theme, selected }) =>
@@ -38,25 +41,14 @@ const StyledHandle = styled(Handle, {
border-color 0.1s;
z-index: 1;
${({ position }) => {
if (position === 'top') {
return css`
transform: translate(-50%, -50%);
transform-origin: top left;
`;
} else if (position === 'bottom') {
return css`
transform: translate(-50%, 50%);
transform-origin: bottom left;
`;
}
}}
transform: translate(-50%, 50%);
transform-origin: bottom left;
&.connectionindicator {
cursor: pointer;
}
${({ disableHoverEffect, theme, position }) => {
${({ disableHoverEffect, theme }) => {
if (disableHoverEffect) {
return undefined;
}
@@ -65,37 +57,27 @@ const StyledHandle = styled(Handle, {
&:hover {
background: ${theme.adaptiveColors.blue1} !important;
border-color: ${theme.color.blue} !important;
${position === 'top' &&
css`
transform: scale(${HANDLE_SCALE_ON_HOVER}) translate(-50%, -50%);
`}
${position === 'bottom' &&
css`
transform: scale(${HANDLE_SCALE_ON_HOVER}) translate(-50%, 50%);
`}
transform: scale(${HANDLE_SCALE_ON_HOVER}) translate(-50%, 50%);
}
`;
}}
}
`;
export const WorkflowDiagramHandleEditable = ({
type,
position,
export const WorkflowDiagramHandleSource = ({
selected,
hovered = false,
}: WorkflowDiagramHandleEditableProps) => {
readOnly = false,
}: WorkflowDiagramHandleSourceProps) => {
const isWorkflowBranchEnabled = useIsFeatureEnabled(
FeatureFlagKey.IS_WORKFLOW_BRANCH_ENABLED,
);
return (
<StyledHandle
type={type}
position={position}
disableHoverEffect={!isWorkflowBranchEnabled}
type={'source'}
position={Position.Bottom}
disableHoverEffect={!isWorkflowBranchEnabled || readOnly}
selected={selected}
hovered={hovered}
/>
@@ -0,0 +1,36 @@
import styled from '@emotion/styled';
import { Handle, Position } from '@xyflow/react';
type WorkflowDiagramHandleTargetProps = {
isConnectable?: boolean;
};
const StyledHandle = styled(Handle)`
&.react-flow__handle {
opacity: 0;
z-index: 1;
border-radius: ${({ theme }) => theme.border.radius.md};
width: 100%;
height: 100%;
left: 0;
top: 0;
transform: translate(-1px, -5px);
&.connectionindicator {
cursor: pointer;
}
}
`;
export const WorkflowDiagramHandleTarget = ({
isConnectable = false,
}: WorkflowDiagramHandleTargetProps) => {
return (
<StyledHandle
type={'target'}
position={Position.Top}
isConnectableEnd={isConnectable}
isConnectableStart={false}
/>
);
};
@@ -1,10 +1,9 @@
import { WorkflowDiagramCreateStepElement } from '@/workflow/workflow-diagram/components/WorkflowDiagramCreateStepElement';
import { WORKFLOW_DIAGRAM_STEP_NODE_BASE_CLICK_OUTSIDE_ID } from '@/workflow/workflow-diagram/constants/WorkflowDiagramStepNodeClickOutsideId';
import { useEdgeState } from '@/workflow/workflow-diagram/hooks/useEdgeState';
import { useEdgeState } from '@/workflow/workflow-diagram/workflow-edges/hooks/useEdgeState';
import { useStartNodeCreation } from '@/workflow/workflow-diagram/hooks/useStartNodeCreation';
import { type WorkflowDiagramStepNodeData } from '@/workflow/workflow-diagram/types/WorkflowDiagram';
import { type WorkflowDiagramNodeVariant } from '@/workflow/workflow-diagram/types/WorkflowDiagramNodeVariant';
import { WorkflowDiagramHandleEditable } from '@/workflow/workflow-diagram/workflow-nodes/components/WorkflowDiagramHandleEditable';
import { WorkflowDiagramStepNodeIcon } from '@/workflow/workflow-diagram/workflow-nodes/components/WorkflowDiagramStepNodeIcon';
import { WorkflowNodeContainer } from '@/workflow/workflow-diagram/workflow-nodes/components/WorkflowNodeContainer';
import { WorkflowNodeIconContainer } from '@/workflow/workflow-diagram/workflow-nodes/components/WorkflowNodeIconContainer';
@@ -13,11 +12,14 @@ import { WorkflowNodeLabelWithCounterPart } from '@/workflow/workflow-diagram/wo
import { WorkflowNodeRightPart } from '@/workflow/workflow-diagram/workflow-nodes/components/WorkflowNodeRightPart';
import { WorkflowNodeTitle } from '@/workflow/workflow-diagram/workflow-nodes/components/WorkflowNodeTitle';
import styled from '@emotion/styled';
import { Position } from '@xyflow/react';
import { useState } from 'react';
import { capitalize } from 'twenty-shared/utils';
import { IconTrash } from 'twenty-ui/display';
import { FloatingIconButton } from 'twenty-ui/input';
import { useConnectionState } from '@/workflow/workflow-diagram/workflow-nodes/hooks/useConnectionState';
import { css } from '@emotion/react';
import { WorkflowDiagramHandleTarget } from '@/workflow/workflow-diagram/workflow-nodes/components/WorkflowDiagramHandleTarget';
import { WorkflowDiagramHandleSource } from '@/workflow/workflow-diagram/workflow-nodes/components/WorkflowDiagramHandleSource';
const StyledAddStepButtonContainer = styled.div<{
shouldDisplay: boolean;
@@ -33,7 +35,9 @@ const StyledAddStepButtonContainer = styled.div<{
transform: translateX(-50%) translateY(100%);
`;
const StyledNodeContainer = styled(WorkflowNodeContainer)`
const StyledNodeContainer = styled(WorkflowNodeContainer)<{
isConnectable?: boolean;
}>`
border-color: ${({ theme }) => theme.border.color.strong};
background: ${({ theme }) => theme.background.secondary};
@@ -44,6 +48,11 @@ const StyledNodeContainer = styled(WorkflowNodeContainer)`
${({ theme }) => theme.background.transparent.lighter} 100%
),
${({ theme }) => theme.background.secondary};
${({ theme, isConnectable }) =>
isConnectable &&
css`
border-color: ${theme.color.blue} !important;
`};
}
.selected & {
@@ -100,30 +109,21 @@ export const WorkflowDiagramStepNodeEditableContent = ({
const { isNodeCreationStarted } = useStartNodeCreation();
const { getNodeHandlesSelectedState, getNodeHandlesHoveredState } =
useEdgeState();
const { isConnectable, isSourceConnected, isInProgressConnection } =
useConnectionState(data.nodeType);
const handlesSelectedState = getNodeHandlesSelectedState(id);
const handlesHoveredState = getNodeHandlesHoveredState(id);
const { isSourceSelected, isSourceHovered } = useEdgeState();
return (
<>
{data.nodeType !== 'trigger' && (
<WorkflowDiagramHandleEditable
type="target"
position={Position.Top}
selected={handlesSelectedState.targetHandle}
hovered={handlesHoveredState.targetHandle}
/>
)}
<StyledNodeContainer
data-click-outside-id={WORKFLOW_DIAGRAM_STEP_NODE_BASE_CLICK_OUTSIDE_ID}
onClick={onClick}
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
isConnectable={isConnectable(id)}
>
<WorkflowDiagramHandleTarget isConnectable={isConnectable(id)} />
<WorkflowNodeIconContainer>
<WorkflowDiagramStepNodeIcon data={data} />
</WorkflowNodeIconContainer>
@@ -147,7 +147,7 @@ export const WorkflowDiagramStepNodeEditableContent = ({
)}
</StyledNodeContainer>
{!data.hasNextStepIds && (
{!data.hasNextStepIds && !isInProgressConnection && (
<StyledAddStepButtonContainer
shouldDisplay={
isHovered ||
@@ -161,11 +161,14 @@ export const WorkflowDiagramStepNodeEditableContent = ({
</StyledAddStepButtonContainer>
)}
<WorkflowDiagramHandleEditable
type="source"
position={Position.Bottom}
selected={handlesSelectedState.sourceHandle || selected}
hovered={handlesHoveredState.sourceHandle}
<WorkflowDiagramHandleSource
selected={
isSourceSelected(id) ||
selected ||
isSourceConnected(id) ||
(isConnectable(id) && isHovered)
}
hovered={isSourceHovered(id) || isHovered}
/>
</>
);
@@ -9,7 +9,6 @@ import { WORKFLOW_DIAGRAM_STEP_NODE_BASE_CLICK_OUTSIDE_ID } from '@/workflow/wor
import { workflowSelectedNodeComponentState } from '@/workflow/workflow-diagram/states/workflowSelectedNodeComponentState';
import { type WorkflowDiagramStepNodeData } from '@/workflow/workflow-diagram/types/WorkflowDiagram';
import { getWorkflowNodeIconKey } from '@/workflow/workflow-diagram/utils/getWorkflowNodeIconKey';
import { WorkflowDiagramHandleReadonly } from '@/workflow/workflow-diagram/workflow-nodes/components/WorkflowDiagramHandleReadonly';
import { WorkflowDiagramStepNodeIcon } from '@/workflow/workflow-diagram/workflow-nodes/components/WorkflowDiagramStepNodeIcon';
import { WorkflowNodeContainer } from '@/workflow/workflow-diagram/workflow-nodes/components/WorkflowNodeContainer';
import { WorkflowNodeIconContainer } from '@/workflow/workflow-diagram/workflow-nodes/components/WorkflowNodeIconContainer';
@@ -18,11 +17,12 @@ import { WorkflowNodeLabelWithCounterPart } from '@/workflow/workflow-diagram/wo
import { WorkflowNodeRightPart } from '@/workflow/workflow-diagram/workflow-nodes/components/WorkflowNodeRightPart';
import { WorkflowNodeTitle } from '@/workflow/workflow-diagram/workflow-nodes/components/WorkflowNodeTitle';
import styled from '@emotion/styled';
import { Position } from '@xyflow/react';
import { useContext } from 'react';
import { useSetRecoilState } from 'recoil';
import { capitalize, isDefined } from 'twenty-shared/utils';
import { useIcons } from 'twenty-ui/display';
import { WorkflowDiagramHandleTarget } from '@/workflow/workflow-diagram/workflow-nodes/components/WorkflowDiagramHandleTarget';
import { WorkflowDiagramHandleSource } from '@/workflow/workflow-diagram/workflow-nodes/components/WorkflowDiagramHandleSource';
const StyledNodeContainer = styled(WorkflowNodeContainer)`
border-color: ${({ theme }) => theme.border.color.strong};
@@ -109,18 +109,11 @@ export const WorkflowDiagramStepNodeReadonly = ({
return (
<>
{data.nodeType !== 'trigger' && (
<WorkflowDiagramHandleReadonly
type="target"
position={Position.Top}
selected={false}
/>
)}
<StyledNodeContainer
data-click-outside-id={WORKFLOW_DIAGRAM_STEP_NODE_BASE_CLICK_OUTSIDE_ID}
onClick={handleClick}
>
<WorkflowDiagramHandleTarget />
<WorkflowNodeIconContainer>
<WorkflowDiagramStepNodeIcon data={data} />
</WorkflowNodeIconContainer>
@@ -134,11 +127,7 @@ export const WorkflowDiagramStepNodeReadonly = ({
</WorkflowNodeRightPart>
</StyledNodeContainer>
<WorkflowDiagramHandleReadonly
type="source"
position={Position.Bottom}
selected={selected}
/>
<WorkflowDiagramHandleSource selected={selected} readOnly />
</>
);
};
@@ -13,6 +13,7 @@ const StyledNodeContainer = styled.div`
box-sizing: border-box;
cursor: pointer;
position: relative;
transition: border-color 0.1s;
`;
export { StyledNodeContainer as WorkflowNodeContainer };
@@ -9,7 +9,6 @@ import { WORKFLOW_DIAGRAM_STEP_NODE_BASE_CLICK_OUTSIDE_ID } from '@/workflow/wor
import { workflowSelectedNodeComponentState } from '@/workflow/workflow-diagram/states/workflowSelectedNodeComponentState';
import { type WorkflowRunDiagramStepNodeData } from '@/workflow/workflow-diagram/types/WorkflowDiagram';
import { getWorkflowNodeIconKey } from '@/workflow/workflow-diagram/utils/getWorkflowNodeIconKey';
import { WorkflowDiagramHandleReadonly } from '@/workflow/workflow-diagram/workflow-nodes/components/WorkflowDiagramHandleReadonly';
import { WorkflowDiagramStepNodeIcon } from '@/workflow/workflow-diagram/workflow-nodes/components/WorkflowDiagramStepNodeIcon';
import { WorkflowNodeContainer } from '@/workflow/workflow-diagram/workflow-nodes/components/WorkflowNodeContainer';
import { WorkflowNodeIconContainer } from '@/workflow/workflow-diagram/workflow-nodes/components/WorkflowNodeIconContainer';
@@ -19,13 +18,14 @@ import { WorkflowNodeRightPart } from '@/workflow/workflow-diagram/workflow-node
import { WorkflowNodeTitle } from '@/workflow/workflow-diagram/workflow-nodes/components/WorkflowNodeTitle';
import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { Position } from '@xyflow/react';
import { useContext } from 'react';
import { useSetRecoilState } from 'recoil';
import { capitalize, isDefined } from 'twenty-shared/utils';
import { StepStatus } from 'twenty-shared/workflow';
import { IconCheck, IconX, useIcons } from 'twenty-ui/display';
import { Loader } from 'twenty-ui/feedback';
import { WorkflowDiagramHandleTarget } from '@/workflow/workflow-diagram/workflow-nodes/components/WorkflowDiagramHandleTarget';
import { WorkflowDiagramHandleSource } from '@/workflow/workflow-diagram/workflow-nodes/components/WorkflowDiagramHandleSource';
const StyledNodeContainer = styled(WorkflowNodeContainer)`
border-color: ${({ theme }) => theme.border.color.strong};
@@ -177,19 +177,12 @@ export const WorkflowRunDiagramStepNode = ({
return (
<>
{data.nodeType !== 'trigger' && (
<WorkflowDiagramHandleReadonly
type="target"
position={Position.Top}
selected={false}
/>
)}
<StyledNodeContainer
data-click-outside-id={WORKFLOW_DIAGRAM_STEP_NODE_BASE_CLICK_OUTSIDE_ID}
data-status={data.runStatus}
onClick={handleClick}
>
<WorkflowDiagramHandleTarget />
<WorkflowNodeIconContainer>
<WorkflowDiagramStepNodeIcon data={data} />
</WorkflowNodeIconContainer>
@@ -230,11 +223,7 @@ export const WorkflowRunDiagramStepNode = ({
</WorkflowNodeRightPart>
</StyledNodeContainer>
<WorkflowDiagramHandleReadonly
type="source"
position={Position.Bottom}
selected={selected}
/>
<WorkflowDiagramHandleSource selected={selected} readOnly />
</>
);
};
@@ -0,0 +1,36 @@
import { useConnection, useEdges } from '@xyflow/react';
export const useConnectionState = (nodeType: 'action' | 'trigger') => {
const connection = useConnection();
const edges = useEdges();
const isInProgressConnection = connection.inProgress;
const isConnectable = (nodeId: string) => {
if (nodeType === 'trigger') {
return false;
}
if (!isInProgressConnection) {
return false;
}
if (connection.fromNode.id === nodeId) {
return false;
}
return !edges.some(
(edge) =>
edge.target === nodeId &&
connection.fromNode?.id &&
edge.source === connection.fromNode.id,
);
};
const isSourceConnected = (nodeId: string) => {
return connection.inProgress && connection.fromNode.id === nodeId;
};
return { isConnectable, isSourceConnected, isInProgressConnection };
};
@@ -1,5 +1,5 @@
import { useGetUpdatableWorkflowVersionOrThrow } from '@/workflow/hooks/useGetUpdatableWorkflowVersionOrThrow';
import { type WorkflowDiagramEdge } from '@/workflow/workflow-diagram/types/WorkflowDiagramEdge';
import { type WorkflowDiagramEdge } from '@/workflow/workflow-diagram/workflow-edges/types/WorkflowDiagramEdge';
import { useCreateWorkflowVersionEdge } from '@/workflow/workflow-steps/hooks/useCreateWorkflowVersionEdge';
import { useState } from 'react';
@@ -1,5 +1,5 @@
import { useGetUpdatableWorkflowVersionOrThrow } from '@/workflow/hooks/useGetUpdatableWorkflowVersionOrThrow';
import { type WorkflowDiagramEdge } from '@/workflow/workflow-diagram/types/WorkflowDiagramEdge';
import { type WorkflowDiagramEdge } from '@/workflow/workflow-diagram/workflow-edges/types/WorkflowDiagramEdge';
import { useDeleteWorkflowVersionEdge } from '@/workflow/workflow-steps/hooks/useDeleteWorkflowVersionEdge';
import { useState } from 'react';