From 0bef766f6359f0023eb3cea777d0e8a87b74775b Mon Sep 17 00:00:00 2001 From: martmull Date: Tue, 5 Aug 2025 16:53:08 +0200 Subject: [PATCH] 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 --- .../components/WorkflowDiagramCanvasBase.tsx | 16 ++++++++++++++ .../WorkflowDiagramDefaultEdgeEditable.tsx | 14 ++++++------- .../WorkflowDiagramFilterEdgeEditable.tsx | 20 ++++-------------- ...owDiagramFilteringDisabledEdgeEditable.tsx | 19 +++++------------ .../hooks/useIsEdgeHovered.ts | 21 +++++++++++++++++++ .../workflowHoveredEdgeIdComponentState.ts | 10 +++++++++ 6 files changed, 62 insertions(+), 38 deletions(-) create mode 100644 packages/twenty-front/src/modules/workflow/workflow-diagram/hooks/useIsEdgeHovered.ts create mode 100644 packages/twenty-front/src/modules/workflow/workflow-diagram/states/workflowHoveredEdgeIdComponentState.ts diff --git a/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowDiagramCanvasBase.tsx b/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowDiagramCanvasBase.tsx index 35089e074c6..f86927e1fa4 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowDiagramCanvasBase.tsx +++ b/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowDiagramCanvasBase.tsx @@ -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, edge: WorkflowDiagramEdge) => { + setEdgeHovered(edge.id); + }, + [setEdgeHovered], + ); + + const onEdgeMouseLeave = useCallback(() => { + setNoEdgeHovered(); + }, [setNoEdgeHovered]); + return ( @@ -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} diff --git a/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowDiagramDefaultEdgeEditable.tsx b/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowDiagramDefaultEdgeEditable.tsx index 96b2a5c4a2f..2b4a5e53591 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowDiagramDefaultEdgeEditable.tsx +++ b/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowDiagramDefaultEdgeEditable.tsx @@ -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; @@ -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)} > ; @@ -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} > - {hovered || isDropdownOpen || isEdgeSelected ? ( + {isEdgeHovered(id) || isDropdownOpen || isEdgeSelected ? ( { 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( diff --git a/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowDiagramFilteringDisabledEdgeEditable.tsx b/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowDiagramFilteringDisabledEdgeEditable.tsx index 4255ee8045b..f1fcb0dec23 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowDiagramFilteringDisabledEdgeEditable.tsx +++ b/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowDiagramFilteringDisabledEdgeEditable.tsx @@ -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; 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} > { + 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 }; +}; diff --git a/packages/twenty-front/src/modules/workflow/workflow-diagram/states/workflowHoveredEdgeIdComponentState.ts b/packages/twenty-front/src/modules/workflow/workflow-diagram/states/workflowHoveredEdgeIdComponentState.ts new file mode 100644 index 00000000000..880d7f6fce6 --- /dev/null +++ b/packages/twenty-front/src/modules/workflow/workflow-diagram/states/workflowHoveredEdgeIdComponentState.ts @@ -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, +});