From 787f8b75abe063a8ba7633ae4414e13de5be9f23 Mon Sep 17 00:00:00 2001 From: martmull Date: Wed, 6 Aug 2025 17:47:35 +0200 Subject: [PATCH] Add design when edge focused (#13687) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fix focused design for edges, edge buttons and handle ## Before image https://github.com/user-attachments/assets/9d702a24-70c4-4f2b-9655-5fec0f83e9d8 ## After Capture d’écran 2025-08-06 à 14 58
54 https://github.com/user-attachments/assets/7d859d12-c2e1-43d5-a611-d906aa9bc0d1 --- .../components/WorkflowDiagramBaseEdge.tsx | 38 ++++++++++++++ .../components/WorkflowDiagramBaseHandle.tsx | 20 +++++--- .../components/WorkflowDiagramCanvasBase.tsx | 13 +++-- .../WorkflowDiagramCanvasEditableEffect.tsx | 40 ++++++++++++++- .../WorkflowDiagramDefaultEdgeEditable.tsx | 49 +++++++------------ .../WorkflowDiagramEdgeButtonGroup.tsx | 34 +++++++++++++ .../WorkflowDiagramEmptyTrigger.tsx | 1 + .../WorkflowDiagramFilterEdgeEditable.tsx | 31 +++++------- ...owDiagramFilteringDisabledEdgeEditable.tsx | 37 +++++--------- .../WorkflowDiagramStepNodeBase.tsx | 9 ++++ .../WorkflowDiagramStepNodeEditable.tsx | 1 + ...WorkflowDiagramStepNodeEditableContent.tsx | 3 ++ .../WorkflowDiagramStepNodeReadonly.tsx | 3 ++ .../WorkflowDiagramEmptyTrigger.stories.tsx | 18 ++++++- ...{useIsEdgeHovered.ts => useEdgeHovered.ts} | 8 ++- .../workflow-diagram/hooks/useEdgeSelected.ts | 47 ++++++++++++++++++ .../workflowSelectedEdgeComponentState.ts | 11 +++++ .../types/WorkflowDiagramEdge.ts | 4 ++ .../types/WorkflowDiagramHandlesSelected.ts | 4 ++ .../transformFilterNodesAsEdges.test.ts | 4 ++ .../utils/transformFilterNodesAsEdges.ts | 1 + .../workflow-steps/hooks/useCreateEdge.ts | 11 ++--- .../workflow-steps/hooks/useDeleteEdge.ts | 11 ++--- 23 files changed, 292 insertions(+), 106 deletions(-) create mode 100644 packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowDiagramBaseEdge.tsx create mode 100644 packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowDiagramEdgeButtonGroup.tsx rename packages/twenty-front/src/modules/workflow/workflow-diagram/hooks/{useIsEdgeHovered.ts => useEdgeHovered.ts} (84%) create mode 100644 packages/twenty-front/src/modules/workflow/workflow-diagram/hooks/useEdgeSelected.ts create mode 100644 packages/twenty-front/src/modules/workflow/workflow-diagram/states/workflowSelectedEdgeComponentState.ts create mode 100644 packages/twenty-front/src/modules/workflow/workflow-diagram/types/WorkflowDiagramEdge.ts create mode 100644 packages/twenty-front/src/modules/workflow/workflow-diagram/types/WorkflowDiagramHandlesSelected.ts diff --git a/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowDiagramBaseEdge.tsx b/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowDiagramBaseEdge.tsx new file mode 100644 index 0000000000..17b5cec112 --- /dev/null +++ b/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowDiagramBaseEdge.tsx @@ -0,0 +1,38 @@ +import { useTheme } from '@emotion/react'; +import { BaseEdge, EdgeProps } from '@xyflow/react'; +import { WorkflowDiagramEdge } from '@/workflow/workflow-diagram/types/WorkflowDiagram'; +import { useEdgeSelected } from '@/workflow/workflow-diagram/hooks/useEdgeSelected'; + +type WorkflowDiagramBaseEdgeProps = Pick< + EdgeProps, + 'source' | 'target' | 'markerStart' | 'markerEnd' +> & { + path: string; +}; + +export const WorkflowDiagramBaseEdge = ({ + source, + target, + markerStart, + markerEnd, + path, +}: WorkflowDiagramBaseEdgeProps) => { + const theme = useTheme(); + + const { isEdgeSelected } = useEdgeSelected(); + + const selected = isEdgeSelected({ source, target }); + + const stroke = selected ? theme.color.blue : theme.border.color.strong; + + return ( + + ); +}; diff --git a/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowDiagramBaseHandle.tsx b/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowDiagramBaseHandle.tsx index f96b75f881..3ab2a46dd1 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowDiagramBaseHandle.tsx +++ b/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowDiagramBaseHandle.tsx @@ -8,6 +8,7 @@ import { FeatureFlagKey } from '~/generated/graphql'; type WorkflowDiagramBaseHandleProps = HandleProps & { isVisible?: boolean; disableHoverEffect?: boolean; + selected?: boolean; }; const HANDLE_SCALE_ON_HOVER = 1.5; @@ -15,13 +16,18 @@ const HANDLE_SCALE_ON_HOVER = 1.5; const TRANSLATE_PERCENT = 33.33; const StyledHandle = styled(Handle, { - shouldForwardProp: (prop) => !['disableHoverEffect'].includes(prop), + shouldForwardProp: (prop) => + !['disableHoverEffect', 'selected'].includes(prop), })` // We need !important to avoid passing style with the Handle.style property height: ${NODE_HANDLE_HEIGHT_PX}px !important; width: ${NODE_HANDLE_WIDTH_PX}px !important; - border-color: ${({ theme }) => theme.border.color.strong} !important; - background: ${({ theme }) => theme.background.primary} !important; + border-color: ${({ theme, selected }) => + selected ? theme.color.blue : theme.border.color.strong} !important; + background: ${({ theme, selected }) => + selected + ? theme.adaptiveColors.blue1 + : theme.background.primary} !important; transition: transform 0.1s ease-out, background 0.1s, @@ -39,7 +45,7 @@ const StyledHandle = styled(Handle, { } }} - ${({ disableHoverEffect, theme, position }) => { + ${({ disableHoverEffect, theme, position, selected }) => { if (disableHoverEffect === true) { return ''; } @@ -54,8 +60,8 @@ const StyledHandle = styled(Handle, { return ` &:hover { - border-color: ${theme.font.color.light} !important; - background: ${theme.background.secondary} !important; + border-color: ${selected ? theme.color.blue : theme.font.color.light} !important; + background: ${selected ? theme.adaptiveColors.blue1 : theme.background.primary} !important; ${transform} } `; @@ -66,6 +72,7 @@ export const WorkflowDiagramBaseHandle = ({ type, position, isVisible = true, + selected = false, }: WorkflowDiagramBaseHandleProps) => { const isWorkflowBranchEnabled = useIsFeatureEnabled( FeatureFlagKey.IS_WORKFLOW_BRANCH_ENABLED, @@ -77,6 +84,7 @@ export const WorkflowDiagramBaseHandle = ({ position={position} style={{ opacity: isVisible ? 1 : 0 }} disableHoverEffect={!isWorkflowBranchEnabled} + selected={selected} /> ); }; 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 b63dcd220e..a82ae3d236 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 @@ -7,7 +7,6 @@ import { useSetRecoilComponentState } from '@/ui/utilities/state/component-state import { getSnapshotValue } from '@/ui/utilities/state/utils/getSnapshotValue'; import { WorkflowDiagramCustomMarkers } from '@/workflow/workflow-diagram/components/WorkflowDiagramCustomMarkers'; import { WorkflowDiagramRightClickCommandMenu } from '@/workflow/workflow-diagram/components/WorkflowDiagramRightClickCommandMenu'; -import { useIsEdgeHovered } from '@/workflow/workflow-diagram/hooks/useIsEdgeHovered'; import { useRightDrawerState } from '@/workflow/workflow-diagram/hooks/useRightDrawerState'; import { workflowDiagramComponentState } from '@/workflow/workflow-diagram/states/workflowDiagramComponentState'; import { workflowDiagramPanOnDragComponentState } from '@/workflow/workflow-diagram/states/workflowDiagramPanOnDragComponentState'; @@ -27,18 +26,18 @@ import { useTheme } from '@emotion/react'; import styled from '@emotion/styled'; import { Background, - Connection, EdgeChange, EdgeProps, FitViewOptions, NodeChange, NodeProps, - OnBeforeDelete, - OnNodeDrag, ReactFlow, applyEdgeChanges, applyNodeChanges, useReactFlow, + Connection, + OnNodeDrag, + OnBeforeDelete, } from '@xyflow/react'; import '@xyflow/react/dist/style.css'; import React, { @@ -54,6 +53,7 @@ import { isDefined } from 'twenty-shared/utils'; import { Tag, TagColor } from 'twenty-ui/components'; import { THEME_COMMON } from 'twenty-ui/theme'; import { FeatureFlagKey } from '~/generated/graphql'; +import { useEdgeHovered } from '@/workflow/workflow-diagram/hooks/useEdgeHovered'; const StyledResetReactflowStyles = styled.div` height: 100%; @@ -191,7 +191,7 @@ export const WorkflowDiagramCanvasBase = ({ workflowDiagramWaitingNodesDimensionsComponentState, ); - const { setEdgeHovered, setNoEdgeHovered } = useIsEdgeHovered(); + const { setEdgeHovered, setNoEdgeHovered } = useEdgeHovered(); const isWorkflowBranchEnabled = useIsFeatureEnabled( FeatureFlagKey.IS_WORKFLOW_BRANCH_ENABLED, @@ -234,6 +234,9 @@ export const WorkflowDiagramCanvasBase = ({ reactflow.setNodes((nodes) => nodes.map((node) => ({ ...node, selected: false })), ); + reactflow.setEdges((edges) => + edges.map((edge) => ({ ...edge, selected: false })), + ); setWorkflowInsertStepIds({ parentStepId: undefined, nextStepId: undefined, diff --git a/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowDiagramCanvasEditableEffect.tsx b/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowDiagramCanvasEditableEffect.tsx index 59f07e9fcd..0ab1d59649 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowDiagramCanvasEditableEffect.tsx +++ b/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowDiagramCanvasEditableEffect.tsx @@ -19,6 +19,9 @@ import { getWorkflowNodeIconKey } from '@/workflow/workflow-diagram/utils/getWor 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 { FeatureFlagKey } from '~/generated/graphql'; export const WorkflowDiagramCanvasEditableEffect = () => { const { getIcon } = useIcons(); @@ -28,6 +31,12 @@ export const WorkflowDiagramCanvasEditableEffect = () => { openWorkflowEditStepInCommandMenu, } = useWorkflowCommandMenu(); + const { setEdgeSelected, clearEdgeSelection } = useEdgeSelected(); + + const isWorkflowBranchEnabled = useIsFeatureEnabled( + FeatureFlagKey.IS_WORKFLOW_BRANCH_ENABLED, + ); + const setWorkflowSelectedNode = useSetRecoilComponentState( workflowSelectedNodeComponentState, ); @@ -42,7 +51,7 @@ export const WorkflowDiagramCanvasEditableEffect = () => { workflowVisualizerWorkflowIdComponentState, ); - const handleSelectionChange = useCallback( + const handleSelectedNodes = useCallback( ({ nodes }: OnSelectionChangeParams) => { const selectedNode = nodes[0] as WorkflowDiagramNode | undefined; @@ -89,6 +98,35 @@ export const WorkflowDiagramCanvasEditableEffect = () => { ], ); + const handleSelectedEdges = useCallback( + ({ edges }: OnSelectionChangeParams) => { + if (!isWorkflowBranchEnabled) { + return; + } + + const selectedEdge = edges?.[0]; + + if (!isDefined(selectedEdge)) { + clearEdgeSelection(); + return; + } + + setEdgeSelected({ + source: selectedEdge.source, + target: selectedEdge.target, + }); + }, + [isWorkflowBranchEnabled, setEdgeSelected, clearEdgeSelection], + ); + + const handleSelectionChange = useCallback( + (onSelectionChangeParams: OnSelectionChangeParams) => { + handleSelectedNodes(onSelectionChangeParams); + handleSelectedEdges(onSelectionChangeParams); + }, + [handleSelectedNodes, handleSelectedEdges], + ); + useOnSelectionChange({ onChange: handleSelectionChange, }); 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 814ca0dfe8..1a51318de0 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 @@ -8,26 +8,15 @@ import { useOpenWorkflowEditFilterInCommandMenu } from '@/workflow/workflow-diag 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 { workflowInsertStepIdsComponentState } from '@/workflow/workflow-steps/states/workflowInsertStepIdsComponentState'; -import { useTheme } from '@emotion/react'; -import styled from '@emotion/styled'; -import { - BaseEdge, - EdgeLabelRenderer, - EdgeProps, - getBezierPath, -} from '@xyflow/react'; +import { useEdgeHovered } from '@/workflow/workflow-diagram/hooks/useEdgeHovered'; +import { EdgeLabelRenderer, EdgeProps, getBezierPath } from '@xyflow/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'; +import { WorkflowDiagramBaseEdge } from '@/workflow/workflow-diagram/components/WorkflowDiagramBaseEdge'; +import { WorkflowDiagramEdgeButtonGroup } from '@/workflow/workflow-diagram/components/WorkflowDiagramEdgeButtonGroup'; type WorkflowDiagramDefaultEdgeEditableProps = EdgeProps; -const StyledIconButtonGroup = styled(IconButtonGroup)` - pointer-events: all; -`; - export const WorkflowDiagramDefaultEdgeEditable = ({ id, source, @@ -39,9 +28,7 @@ export const WorkflowDiagramDefaultEdgeEditable = ({ markerStart, markerEnd, }: WorkflowDiagramDefaultEdgeEditableProps) => { - const theme = useTheme(); - - const { isEdgeHovered } = useIsEdgeHovered(); + const { isEdgeHovered } = useEdgeHovered(); const [edgePath, labelX, labelY] = getBezierPath({ sourceX, @@ -57,15 +44,12 @@ export const WorkflowDiagramDefaultEdgeEditable = ({ const { createStep } = useCreateStep({ workflow }); - const { startNodeCreation } = useStartNodeCreation(); + const { startNodeCreation, isNodeCreationStarted } = useStartNodeCreation(); - const workflowInsertStepIds = useRecoilComponentValue( - workflowInsertStepIdsComponentState, - ); - - const isSelected = - workflowInsertStepIds.nextStepId === target && - workflowInsertStepIds.parentStepId === source; + const nodeCreationStarted = isNodeCreationStarted({ + parentStepId: source, + nextStepId: target, + }); const { openWorkflowEditFilterInCommandMenu } = useOpenWorkflowEditFilterInCommandMenu(); @@ -97,11 +81,12 @@ export const WorkflowDiagramDefaultEdgeEditable = ({ return ( <> - @@ -111,10 +96,9 @@ export const WorkflowDiagramDefaultEdgeEditable = ({ labelY={labelY} > - diff --git a/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowDiagramEdgeButtonGroup.tsx b/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowDiagramEdgeButtonGroup.tsx new file mode 100644 index 0000000000..3c07a8b884 --- /dev/null +++ b/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowDiagramEdgeButtonGroup.tsx @@ -0,0 +1,34 @@ +import styled from '@emotion/styled'; +import { IconButtonGroup, IconButtonGroupProps } from 'twenty-ui/input'; +import { getWorkflowDiagramNodeSelectedColors } from '@/workflow/workflow-diagram/utils/getWorkflowDiagramNodeSelectedColors'; +import { css } from '@emotion/react'; + +const StyledIconButtonGroup = styled(IconButtonGroup)<{ selected?: boolean }>` + pointer-events: all; + + ${({ selected, theme }) => { + if (!selected) return ''; + const colors = getWorkflowDiagramNodeSelectedColors('default', theme); + return css` + background-color: ${colors.background}; + border: 1px solid ${colors.borderColor}; + `; + }} +`; + +type WorkflowDiagramEdgeButtonGroupProps = IconButtonGroupProps & { + selected?: boolean; +}; + +export const WorkflowDiagramEdgeButtonGroup = ({ + selected = false, + iconButtons, +}: WorkflowDiagramEdgeButtonGroupProps) => { + return ( + + ); +}; diff --git a/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowDiagramEmptyTrigger.tsx b/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowDiagramEmptyTrigger.tsx index dd1528b8f7..1dd4710f49 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowDiagramEmptyTrigger.tsx +++ b/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowDiagramEmptyTrigger.tsx @@ -13,6 +13,7 @@ const StyledStepNodeLabelIconContainer = styled.div` export const WorkflowDiagramEmptyTrigger = () => { return ( ; @@ -86,8 +83,6 @@ export const WorkflowDiagramFilterEdgeEditable = ({ }: WorkflowDiagramFilterEdgeEditableProps) => { assertFilterEdgeDataOrThrow(data); - const theme = useTheme(); - const [edgePath, labelX, labelY] = getBezierPath({ sourceX, sourceY, @@ -106,13 +101,13 @@ export const WorkflowDiagramFilterEdgeEditable = ({ const { openDropdown } = useOpenDropdown(); const { closeDropdown } = useCloseDropdown(); - const { isEdgeHovered } = useIsEdgeHovered(); + const { isEdgeHovered } = useEdgeHovered(); const setWorkflowDiagramPanOnDrag = useSetRecoilComponentState( workflowDiagramPanOnDragComponentState, ); - const isEdgeSelected = isNodeCreationStarted({ + const nodeCreationStarted = isNodeCreationStarted({ parentStepId: data.stepId, nextStepId: target, }); @@ -153,11 +148,12 @@ export const WorkflowDiagramFilterEdgeEditable = ({ return ( <> - @@ -168,9 +164,8 @@ export const WorkflowDiagramFilterEdgeEditable = ({ > - {isEdgeHovered(id) || isDropdownOpen || isEdgeSelected ? ( - ; @@ -33,9 +23,7 @@ export const WorkflowDiagramFilteringDisabledEdgeEditable = ({ targetX, targetY, }: WorkflowDiagramFilteringDisabledEdgeEditableProps) => { - const theme = useTheme(); - - const { isEdgeHovered } = useIsEdgeHovered(); + const { isEdgeHovered } = useEdgeHovered(); const [edgePath, labelX, labelY] = getBezierPath({ sourceX, @@ -46,7 +34,7 @@ export const WorkflowDiagramFilteringDisabledEdgeEditable = ({ const { startNodeCreation, isNodeCreationStarted } = useStartNodeCreation(); - const forceDisplayAddButton = isNodeCreationStarted({ + const nodeCreationStarted = isNodeCreationStarted({ parentStepId: source, nextStepId: target, }); @@ -61,11 +49,12 @@ export const WorkflowDiagramFilteringDisabledEdgeEditable = ({ return ( <> - @@ -75,16 +64,16 @@ export const WorkflowDiagramFilteringDisabledEdgeEditable = ({ labelY={labelY} > - diff --git a/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowDiagramStepNodeBase.tsx b/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowDiagramStepNodeBase.tsx index d2f3af508c..24878eeb32 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowDiagramStepNodeBase.tsx +++ b/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowDiagramStepNodeBase.tsx @@ -11,6 +11,7 @@ import { Label, OverflowingTextWithTooltip } from 'twenty-ui/display'; import { Loader } from 'twenty-ui/feedback'; import { WorkflowDiagramBaseHandle } from '@/workflow/workflow-diagram/components/WorkflowDiagramBaseHandle'; import { Position } from '@xyflow/react'; +import { useEdgeSelected } from '@/workflow/workflow-diagram/hooks/useEdgeSelected'; const StyledStepNodeContainer = styled.div` display: flex; @@ -141,6 +142,7 @@ const StyledStepNodeLabel = styled.div<{ `; export const WorkflowDiagramStepNodeBase = ({ + id, nodeType, name, variant, @@ -151,6 +153,7 @@ export const WorkflowDiagramStepNodeBase = ({ onMouseEnter, onMouseLeave, }: { + id: string; nodeType: WorkflowDiagramStepNodeData['nodeType']; name: string; variant: WorkflowDiagramNodeVariant; @@ -161,6 +164,10 @@ export const WorkflowDiagramStepNodeBase = ({ onMouseEnter?: (event: MouseEvent) => void; onMouseLeave?: (event: MouseEvent) => void; }) => { + const { getNodeHandlesSelectedState } = useEdgeSelected(); + + const handlesSelectedState = getNodeHandlesSelectedState(id); + return ( )} @@ -196,6 +204,7 @@ export const WorkflowDiagramStepNodeBase = ({ type="source" position={Position.Bottom} isVisible={displayHandle} + selected={handlesSelectedState.sourceHandle} /> ); diff --git a/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowDiagramStepNodeEditable.tsx b/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowDiagramStepNodeEditable.tsx index 34273ed3d9..323f40e9d0 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowDiagramStepNodeEditable.tsx +++ b/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowDiagramStepNodeEditable.tsx @@ -30,6 +30,7 @@ export const WorkflowDiagramStepNodeEditable = ({ return ( { return ( = { title: 'Modules/Workflow/WorkflowDiagramEmptyTrigger', @@ -22,7 +24,13 @@ export const Default: Story = { decorators: [ (Story) => (
- + + + + +
), ReactflowDecorator, @@ -34,7 +42,13 @@ export const Selected: Story = { decorators: [ (Story) => (
- + + + + +
), ReactflowDecorator, diff --git a/packages/twenty-front/src/modules/workflow/workflow-diagram/hooks/useIsEdgeHovered.ts b/packages/twenty-front/src/modules/workflow/workflow-diagram/hooks/useEdgeHovered.ts similarity index 84% rename from packages/twenty-front/src/modules/workflow/workflow-diagram/hooks/useIsEdgeHovered.ts rename to packages/twenty-front/src/modules/workflow/workflow-diagram/hooks/useEdgeHovered.ts index 01a12cc4fa..750dd16b82 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-diagram/hooks/useIsEdgeHovered.ts +++ b/packages/twenty-front/src/modules/workflow/workflow-diagram/hooks/useEdgeHovered.ts @@ -1,7 +1,7 @@ import { useRecoilComponentState } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentState'; import { workflowHoveredEdgeIdComponentState } from '@/workflow/workflow-diagram/states/workflowHoveredEdgeIdComponentState'; -export const useIsEdgeHovered = () => { +export const useEdgeHovered = () => { const [workflowHoveredEdgeId, setWorkflowHoveredEdgeId] = useRecoilComponentState(workflowHoveredEdgeIdComponentState); @@ -17,5 +17,9 @@ export const useIsEdgeHovered = () => { setWorkflowHoveredEdgeId(undefined); }; - return { isEdgeHovered, setEdgeHovered, setNoEdgeHovered }; + return { + isEdgeHovered, + setEdgeHovered, + setNoEdgeHovered, + }; }; diff --git a/packages/twenty-front/src/modules/workflow/workflow-diagram/hooks/useEdgeSelected.ts b/packages/twenty-front/src/modules/workflow/workflow-diagram/hooks/useEdgeSelected.ts new file mode 100644 index 0000000000..24dc0f777b --- /dev/null +++ b/packages/twenty-front/src/modules/workflow/workflow-diagram/hooks/useEdgeSelected.ts @@ -0,0 +1,47 @@ +import { workflowSelectedEdgeComponentState } from '@/workflow/workflow-diagram/states/workflowSelectedEdgeComponentState'; +import { useRecoilComponentState } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentState'; +import { WorkflowDiagramHandlesSelected } from '@/workflow/workflow-diagram/types/WorkflowDiagramHandlesSelected'; +import { WorkflowDiagramEdge } from '@/workflow/workflow-diagram/types/WorkflowDiagramEdge'; + +export const useEdgeSelected = () => { + const [workflowSelectedEdge, setWorkflowSelectedEdge] = + useRecoilComponentState(workflowSelectedEdgeComponentState); + + const isEdgeSelected = ({ source, target }: WorkflowDiagramEdge) => { + return ( + workflowSelectedEdge?.source === source && + workflowSelectedEdge?.target === target + ); + }; + + const getNodeHandlesSelectedState = ( + id: string, + ): WorkflowDiagramHandlesSelected => { + return { + targetHandle: workflowSelectedEdge?.target === id, + sourceHandle: workflowSelectedEdge?.source === id, + }; + }; + + const setEdgeSelected = ({ source, target }: WorkflowDiagramEdge) => { + if ( + workflowSelectedEdge?.source === source && + workflowSelectedEdge?.target === target + ) { + return; + } + + setWorkflowSelectedEdge({ source, target }); + }; + + const clearEdgeSelection = () => { + setWorkflowSelectedEdge(undefined); + }; + + return { + isEdgeSelected, + setEdgeSelected, + clearEdgeSelection, + getNodeHandlesSelectedState, + }; +}; diff --git a/packages/twenty-front/src/modules/workflow/workflow-diagram/states/workflowSelectedEdgeComponentState.ts b/packages/twenty-front/src/modules/workflow/workflow-diagram/states/workflowSelectedEdgeComponentState.ts new file mode 100644 index 0000000000..b0757c86bd --- /dev/null +++ b/packages/twenty-front/src/modules/workflow/workflow-diagram/states/workflowSelectedEdgeComponentState.ts @@ -0,0 +1,11 @@ +import { createComponentState } from '@/ui/utilities/state/component-state/utils/createComponentState'; +import { WorkflowVisualizerComponentInstanceContext } from '@/workflow/workflow-diagram/states/contexts/WorkflowVisualizerComponentInstanceContext'; +import { WorkflowDiagramEdge } from '@/workflow/workflow-diagram/types/WorkflowDiagramEdge'; + +export const workflowSelectedEdgeComponentState = createComponentState< + WorkflowDiagramEdge | undefined +>({ + key: 'workflowSelectedEdgeComponentState', + defaultValue: undefined, + componentInstanceContext: WorkflowVisualizerComponentInstanceContext, +}); diff --git a/packages/twenty-front/src/modules/workflow/workflow-diagram/types/WorkflowDiagramEdge.ts b/packages/twenty-front/src/modules/workflow/workflow-diagram/types/WorkflowDiagramEdge.ts new file mode 100644 index 0000000000..dcd2518a4f --- /dev/null +++ b/packages/twenty-front/src/modules/workflow/workflow-diagram/types/WorkflowDiagramEdge.ts @@ -0,0 +1,4 @@ +export type WorkflowDiagramEdge = { + source: string; + target: string; +}; diff --git a/packages/twenty-front/src/modules/workflow/workflow-diagram/types/WorkflowDiagramHandlesSelected.ts b/packages/twenty-front/src/modules/workflow/workflow-diagram/types/WorkflowDiagramHandlesSelected.ts new file mode 100644 index 0000000000..17362dd078 --- /dev/null +++ b/packages/twenty-front/src/modules/workflow/workflow-diagram/types/WorkflowDiagramHandlesSelected.ts @@ -0,0 +1,4 @@ +export type WorkflowDiagramHandlesSelected = { + targetHandle: boolean; + sourceHandle: boolean; +}; diff --git a/packages/twenty-front/src/modules/workflow/workflow-diagram/utils/__tests__/transformFilterNodesAsEdges.test.ts b/packages/twenty-front/src/modules/workflow/workflow-diagram/utils/__tests__/transformFilterNodesAsEdges.test.ts index 71814bb9e5..59761a0905 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-diagram/utils/__tests__/transformFilterNodesAsEdges.test.ts +++ b/packages/twenty-front/src/modules/workflow/workflow-diagram/utils/__tests__/transformFilterNodesAsEdges.test.ts @@ -149,6 +149,7 @@ describe('transformFilterNodesAsEdges', () => { type: 'filter--editable', source: 'A', target: 'C', + deletable: false, data: { edgeType: 'filter', stepId: 'B', @@ -274,6 +275,7 @@ describe('transformFilterNodesAsEdges', () => { type: 'filter--editable', source: 'A', target: 'C', + deletable: false, data: { edgeType: 'filter', name: 'Filter B1', @@ -291,6 +293,7 @@ describe('transformFilterNodesAsEdges', () => { type: 'filter--editable', source: 'C', target: 'D', + deletable: false, data: { edgeType: 'filter', name: 'Filter B2', @@ -462,6 +465,7 @@ describe('transformFilterNodesAsEdges', () => { type: 'filter--editable', source: 'trigger', target: 'C', + deletable: false, data: { edgeType: 'filter', name: 'Filter B', diff --git a/packages/twenty-front/src/modules/workflow/workflow-diagram/utils/transformFilterNodesAsEdges.ts b/packages/twenty-front/src/modules/workflow/workflow-diagram/utils/transformFilterNodesAsEdges.ts index a5e6bbb99d..dc4ae832f4 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-diagram/utils/transformFilterNodesAsEdges.ts +++ b/packages/twenty-front/src/modules/workflow/workflow-diagram/utils/transformFilterNodesAsEdges.ts @@ -53,6 +53,7 @@ export const transformFilterNodesAsEdges = < type: defaultFilterEdgeType, id: `${incomingEdge.source}-${outgoingEdge.target}-filter-${filterNode.id}`, target: outgoingEdge.target, + deletable: false, data: { ...incomingEdge.data, edgeType: 'filter', diff --git a/packages/twenty-front/src/modules/workflow/workflow-steps/hooks/useCreateEdge.ts b/packages/twenty-front/src/modules/workflow/workflow-steps/hooks/useCreateEdge.ts index c0e17dc170..127090396a 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-steps/hooks/useCreateEdge.ts +++ b/packages/twenty-front/src/modules/workflow/workflow-steps/hooks/useCreateEdge.ts @@ -3,6 +3,7 @@ import { WorkflowWithCurrentVersion } from '@/workflow/types/Workflow'; import { useCreateWorkflowVersionEdge } from '@/workflow/workflow-steps/hooks/useCreateWorkflowVersionEdge'; import { useState } from 'react'; import { isDefined } from 'twenty-shared/utils'; +import { WorkflowDiagramEdge } from '@/workflow/workflow-diagram/types/WorkflowDiagramEdge'; export const useCreateEdge = ({ workflow, @@ -15,14 +16,8 @@ export const useCreateEdge = ({ const { getUpdatableWorkflowVersion } = useGetUpdatableWorkflowVersion(); - const createEdge = async ({ - source, - target, - }: { - source: string; - target: string; - }) => { - if (isLoading === true) { + const createEdge = async ({ source, target }: WorkflowDiagramEdge) => { + if (isLoading) { return; } diff --git a/packages/twenty-front/src/modules/workflow/workflow-steps/hooks/useDeleteEdge.ts b/packages/twenty-front/src/modules/workflow/workflow-steps/hooks/useDeleteEdge.ts index c3c6163682..9b932f5cf1 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-steps/hooks/useDeleteEdge.ts +++ b/packages/twenty-front/src/modules/workflow/workflow-steps/hooks/useDeleteEdge.ts @@ -3,6 +3,7 @@ import { WorkflowWithCurrentVersion } from '@/workflow/types/Workflow'; import { useDeleteWorkflowVersionEdge } from '@/workflow/workflow-steps/hooks/useDeleteWorkflowVersionEdge'; import { useState } from 'react'; import { isDefined } from 'twenty-shared/utils'; +import { WorkflowDiagramEdge } from '@/workflow/workflow-diagram/types/WorkflowDiagramEdge'; export const useDeleteEdge = ({ workflow, @@ -15,14 +16,8 @@ export const useDeleteEdge = ({ const { getUpdatableWorkflowVersion } = useGetUpdatableWorkflowVersion(); - const deleteEdge = async ({ - source, - target, - }: { - source: string; - target: string; - }) => { - if (isLoading === true) { + const deleteEdge = async ({ source, target }: WorkflowDiagramEdge) => { + if (isLoading) { return; }