Display edge button when whole edge is hovered (#13651)

As title

## Before
Need to aim center of edge


https://github.com/user-attachments/assets/5f014f15-af7b-4884-902d-3b7001782d2d


## After
Whole edge triggers hover behavior


https://github.com/user-attachments/assets/bddaa6b2-7d62-428a-bee5-c237db16a403
This commit is contained in:
martmull
2025-08-05 14:53:08 +00:00
committed by GitHub
parent c60da685f7
commit 0bef766f63
6 changed files with 62 additions and 38 deletions
@@ -53,6 +53,7 @@ import { WorkflowDiagramRightClickCommandMenu } from '@/workflow/workflow-diagra
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
import { FeatureFlagKey } from '~/generated/graphql';
import { getOrganizedDiagram } from '@/workflow/workflow-diagram/utils/getOrganizedDiagram';
import { useIsEdgeHovered } from '@/workflow/workflow-diagram/hooks/useIsEdgeHovered';
const StyledResetReactflowStyles = styled.div`
height: 100%;
@@ -188,6 +189,8 @@ export const WorkflowDiagramCanvasBase = ({
workflowDiagramWaitingNodesDimensionsComponentState,
);
const { setEdgeHovered, setNoEdgeHovered } = useIsEdgeHovered();
const isWorkflowBranchEnabled = useIsFeatureEnabled(
FeatureFlagKey.IS_WORKFLOW_BRANCH_ENABLED,
);
@@ -436,6 +439,17 @@ export const WorkflowDiagramCanvasBase = ({
[handlePaneContextMenu],
);
const onEdgeMouseEnter = useCallback(
(_: React.MouseEvent<Element, MouseEvent>, edge: WorkflowDiagramEdge) => {
setEdgeHovered(edge.id);
},
[setEdgeHovered],
);
const onEdgeMouseLeave = useCallback(() => {
setNoEdgeHovered();
}, [setNoEdgeHovered]);
return (
<StyledResetReactflowStyles ref={containerRef}>
<WorkflowDiagramCustomMarkers />
@@ -449,6 +463,8 @@ export const WorkflowDiagramCanvasBase = ({
edgeTypes={edgeTypes}
nodes={nodes}
edges={edges}
onEdgeMouseEnter={onEdgeMouseEnter}
onEdgeMouseLeave={onEdgeMouseLeave}
onNodesChange={handleNodesChanges}
onEdgesChange={handleEdgesChange}
onConnect={isWorkflowBranchEnabled ? onConnect : undefined}
@@ -17,10 +17,10 @@ import {
EdgeProps,
getBezierPath,
} from '@xyflow/react';
import { useState } from 'react';
import { isDefined } from 'twenty-shared/utils';
import { IconFilter, IconPlus } from 'twenty-ui/display';
import { IconButtonGroup } from 'twenty-ui/input';
import { useIsEdgeHovered } from '@/workflow/workflow-diagram/hooks/useIsEdgeHovered';
type WorkflowDiagramDefaultEdgeEditableProps = EdgeProps<WorkflowDiagramEdge>;
@@ -29,6 +29,7 @@ const StyledIconButtonGroup = styled(IconButtonGroup)`
`;
export const WorkflowDiagramDefaultEdgeEditable = ({
id,
source,
target,
sourceX,
@@ -40,6 +41,8 @@ export const WorkflowDiagramDefaultEdgeEditable = ({
}: WorkflowDiagramDefaultEdgeEditableProps) => {
const theme = useTheme();
const { isEdgeHovered } = useIsEdgeHovered();
const [edgePath, labelX, labelY] = getBezierPath({
sourceX,
sourceY,
@@ -53,9 +56,8 @@ export const WorkflowDiagramDefaultEdgeEditable = ({
const workflow = useWorkflowWithCurrentVersion(workflowVisualizerWorkflowId);
const { createStep } = useCreateStep({ workflow });
const { startNodeCreation } = useStartNodeCreation();
const [hovered, setHovered] = useState(false);
const { startNodeCreation } = useStartNodeCreation();
const workflowInsertStepIds = useRecoilComponentValueV2(
workflowInsertStepIdsComponentState,
@@ -83,8 +85,6 @@ export const WorkflowDiagramDefaultEdgeEditable = ({
stepId: createdStep.id,
stepName: createdStep.name,
});
setHovered(false);
};
const handleNodeButtonClick = () => {
@@ -109,11 +109,9 @@ export const WorkflowDiagramDefaultEdgeEditable = ({
data-click-outside-id={WORKFLOW_DIAGRAM_EDGE_OPTIONS_CLICK_OUTSIDE_ID}
labelX={labelX}
labelY={labelY}
onMouseEnter={() => setHovered(true)}
onMouseLeave={() => setHovered(false)}
>
<WorkflowDiagramEdgeV2VisibilityContainer
shouldDisplay={isSelected || hovered}
shouldDisplay={isSelected || isEdgeHovered(id)}
>
<StyledIconButtonGroup
className="nodrag nopan"
@@ -31,7 +31,6 @@ import {
EdgeProps,
getBezierPath,
} from '@xyflow/react';
import { useState } from 'react';
import { isDefined } from 'twenty-shared/utils';
import {
IconDotsVertical,
@@ -41,6 +40,7 @@ import {
} from 'twenty-ui/display';
import { IconButtonGroup } from 'twenty-ui/input';
import { MenuItem } from 'twenty-ui/navigation';
import { useIsEdgeHovered } from '@/workflow/workflow-diagram/hooks/useIsEdgeHovered';
type WorkflowDiagramFilterEdgeEditableProps = EdgeProps<WorkflowDiagramEdge>;
@@ -73,6 +73,7 @@ const StyledConfiguredFilterContainer = styled.div`
`;
export const WorkflowDiagramFilterEdgeEditable = ({
id,
source,
target,
sourceY,
@@ -105,7 +106,7 @@ export const WorkflowDiagramFilterEdgeEditable = ({
const { openDropdown } = useOpenDropdown();
const { closeDropdown } = useCloseDropdown();
const [hovered, setHovered] = useState(false);
const { isEdgeHovered } = useIsEdgeHovered();
const setWorkflowDiagramPanOnDrag = useSetRecoilComponentStateV2(
workflowDiagramPanOnDragComponentState,
@@ -133,14 +134,6 @@ export const WorkflowDiagramFilterEdgeEditable = ({
const { openWorkflowEditFilterInCommandMenu } =
useOpenWorkflowEditFilterInCommandMenu();
const handleMouseEnter = () => {
setHovered(true);
};
const handleMouseLeave = () => {
setHovered(false);
};
const handleFilterButtonClick = () => {
openWorkflowEditFilterInCommandMenu({
stepId: data.stepId,
@@ -150,7 +143,6 @@ export const WorkflowDiagramFilterEdgeEditable = ({
const handleAddNodeButtonClick = () => {
closeDropdown(dropdownId);
setHovered(false);
startNodeCreation({
parentStepId: data.stepId,
@@ -173,12 +165,10 @@ export const WorkflowDiagramFilterEdgeEditable = ({
data-click-outside-id={WORKFLOW_DIAGRAM_EDGE_OPTIONS_CLICK_OUTSIDE_ID}
labelX={labelX}
labelY={labelY}
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
>
<WorkflowDiagramEdgeV2VisibilityContainer shouldDisplay>
<StyledConfiguredFilterContainer>
{hovered || isDropdownOpen || isEdgeSelected ? (
{isEdgeHovered(id) || isDropdownOpen || isEdgeSelected ? (
<StyledIconButtonGroup
className="nodrag nopan"
iconButtons={[
@@ -237,7 +227,6 @@ export const WorkflowDiagramFilterEdgeEditable = ({
LeftIcon={IconFilter}
onClick={() => {
closeDropdown(dropdownId);
setHovered(false);
handleFilterButtonClick();
}}
@@ -247,7 +236,6 @@ export const WorkflowDiagramFilterEdgeEditable = ({
LeftIcon={IconFilterX}
onClick={() => {
closeDropdown(dropdownId);
setHovered(false);
if (!isDefined(data.stepId)) {
throw new Error(
@@ -9,11 +9,11 @@ import {
EdgeProps,
getBezierPath,
} from '@xyflow/react';
import { useState } from 'react';
import { IconPlus } from 'twenty-ui/display';
import { IconButtonGroup } from 'twenty-ui/input';
import { WorkflowDiagramEdgeV2Container } from '@/workflow/workflow-diagram/components/WorkflowDiagramEdgeV2Container';
import { WorkflowDiagramEdgeV2VisibilityContainer } from '@/workflow/workflow-diagram/components/WorkflowDiagramEdgeV2VisibilityContainer';
import { useIsEdgeHovered } from '@/workflow/workflow-diagram/hooks/useIsEdgeHovered';
const StyledIconButtonGroup = styled(IconButtonGroup)`
pointer-events: all;
@@ -23,6 +23,7 @@ type WorkflowDiagramFilteringDisabledEdgeEditableProps =
EdgeProps<WorkflowDiagramEdge>;
export const WorkflowDiagramFilteringDisabledEdgeEditable = ({
id,
markerStart,
markerEnd,
source,
@@ -34,6 +35,8 @@ export const WorkflowDiagramFilteringDisabledEdgeEditable = ({
}: WorkflowDiagramFilteringDisabledEdgeEditableProps) => {
const theme = useTheme();
const { isEdgeHovered } = useIsEdgeHovered();
const [edgePath, labelX, labelY] = getBezierPath({
sourceX,
sourceY,
@@ -41,16 +44,6 @@ export const WorkflowDiagramFilteringDisabledEdgeEditable = ({
targetY,
});
const [hovered, setHovered] = useState(false);
const handleMouseEnter = () => {
setHovered(true);
};
const handleMouseLeave = () => {
setHovered(false);
};
const { startNodeCreation, isNodeCreationStarted } = useStartNodeCreation();
const forceDisplayAddButton = isNodeCreationStarted({
@@ -80,11 +73,9 @@ export const WorkflowDiagramFilteringDisabledEdgeEditable = ({
data-click-outside-id={WORKFLOW_DIAGRAM_EDGE_OPTIONS_CLICK_OUTSIDE_ID}
labelX={labelX}
labelY={labelY}
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
>
<WorkflowDiagramEdgeV2VisibilityContainer
shouldDisplay={hovered || forceDisplayAddButton}
shouldDisplay={isEdgeHovered(id) || forceDisplayAddButton}
>
<StyledIconButtonGroup
className="nodrag nopan"
@@ -0,0 +1,21 @@
import { workflowHoveredEdgeIdComponentState } from '@/workflow/workflow-diagram/states/workflowHoveredEdgeIdComponentState';
import { useRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentStateV2';
export const useIsEdgeHovered = () => {
const [workflowHoveredEdgeId, setWorkflowHoveredEdgeId] =
useRecoilComponentStateV2(workflowHoveredEdgeIdComponentState);
const isEdgeHovered = (edgeId: string) => {
return workflowHoveredEdgeId === edgeId;
};
const setEdgeHovered = (edgeId: string) => {
setWorkflowHoveredEdgeId(edgeId);
};
const setNoEdgeHovered = () => {
setWorkflowHoveredEdgeId(undefined);
};
return { isEdgeHovered, setEdgeHovered, setNoEdgeHovered };
};
@@ -0,0 +1,10 @@
import { createComponentStateV2 } from '@/ui/utilities/state/component-state/utils/createComponentStateV2';
import { WorkflowVisualizerComponentInstanceContext } from '@/workflow/workflow-diagram/states/contexts/WorkflowVisualizerComponentInstanceContext';
export const workflowHoveredEdgeIdComponentState = createComponentStateV2<
string | undefined
>({
key: 'workflowHoveredEdgeIdComponentState',
defaultValue: undefined,
componentInstanceContext: WorkflowVisualizerComponentInstanceContext,
});