Add design when edge focused (#13687)

Fix focused design for edges, edge buttons and handle

## Before
<img width="3014" height="1026" alt="image"
src="https://github.com/user-attachments/assets/8ef6647b-1ba6-4c71-9ffd-f6f5fe15e891"
/>


https://github.com/user-attachments/assets/9d702a24-70c4-4f2b-9655-5fec0f83e9d8


## After

<img width="1509" height="595" alt="Capture d’écran 2025-08-06 à 14 58
54"
src="https://github.com/user-attachments/assets/7c81b504-69a3-4d62-b455-738764b21d45"
/>



https://github.com/user-attachments/assets/7d859d12-c2e1-43d5-a611-d906aa9bc0d1
This commit is contained in:
martmull
2025-08-06 17:47:35 +02:00
committed by GitHub
parent b71e6b0609
commit 787f8b75ab
23 changed files with 292 additions and 106 deletions
@@ -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<WorkflowDiagramEdge>,
'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 (
<BaseEdge
markerStart={markerStart}
markerEnd={markerEnd}
path={path}
style={{
stroke,
}}
/>
);
};
@@ -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),
})<WorkflowDiagramBaseHandleProps>`
// 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}
/>
);
};
@@ -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,
@@ -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,
});
@@ -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<WorkflowDiagramEdge>;
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 (
<>
<BaseEdge
<WorkflowDiagramBaseEdge
source={source}
target={target}
path={edgePath}
markerStart={markerStart}
markerEnd={markerEnd}
path={edgePath}
style={{ stroke: theme.border.color.strong }}
/>
<EdgeLabelRenderer>
@@ -111,10 +96,9 @@ export const WorkflowDiagramDefaultEdgeEditable = ({
labelY={labelY}
>
<WorkflowDiagramEdgeV2VisibilityContainer
shouldDisplay={isSelected || isEdgeHovered(id)}
shouldDisplay={nodeCreationStarted || isEdgeHovered(id)}
>
<StyledIconButtonGroup
className="nodrag nopan"
<WorkflowDiagramEdgeButtonGroup
iconButtons={[
{
Icon: IconFilter,
@@ -125,6 +109,7 @@ export const WorkflowDiagramDefaultEdgeEditable = ({
onClick: handleNodeButtonClick,
},
]}
selected={nodeCreationStarted}
/>
</WorkflowDiagramEdgeV2VisibilityContainer>
</WorkflowDiagramEdgeV2Container>
@@ -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 (
<StyledIconButtonGroup
className="nodrag nopan"
iconButtons={iconButtons}
selected={selected}
/>
);
};
@@ -13,6 +13,7 @@ const StyledStepNodeLabelIconContainer = styled.div`
export const WorkflowDiagramEmptyTrigger = () => {
return (
<WorkflowDiagramStepNodeBase
id={'trigger'}
name="Add a Trigger"
nodeType="trigger"
variant="empty"
@@ -22,15 +22,10 @@ import {
} from '@/workflow/workflow-diagram/types/WorkflowDiagram';
import { getWorkflowDiagramNodeSelectedColors } from '@/workflow/workflow-diagram/utils/getWorkflowDiagramNodeSelectedColors';
import { useDeleteStep } from '@/workflow/workflow-steps/hooks/useDeleteStep';
import { css, useTheme } from '@emotion/react';
import { css } from '@emotion/react';
import styled from '@emotion/styled';
import { isNonEmptyString } from '@sniptt/guards';
import {
BaseEdge,
EdgeLabelRenderer,
EdgeProps,
getBezierPath,
} from '@xyflow/react';
import { EdgeLabelRenderer, EdgeProps, getBezierPath } from '@xyflow/react';
import { isDefined } from 'twenty-shared/utils';
import {
IconDotsVertical,
@@ -40,7 +35,9 @@ 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';
import { useEdgeHovered } from '@/workflow/workflow-diagram/hooks/useEdgeHovered';
import { WorkflowDiagramBaseEdge } from '@/workflow/workflow-diagram/components/WorkflowDiagramBaseEdge';
import { WorkflowDiagramEdgeButtonGroup } from '@/workflow/workflow-diagram/components/WorkflowDiagramEdgeButtonGroup';
type WorkflowDiagramFilterEdgeEditableProps = EdgeProps<WorkflowDiagramEdge>;
@@ -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 (
<>
<BaseEdge
<WorkflowDiagramBaseEdge
source={source}
target={target}
path={edgePath}
markerStart={markerStart}
markerEnd={markerEnd}
path={edgePath}
style={{ stroke: theme.border.color.strong }}
/>
<EdgeLabelRenderer>
@@ -168,9 +164,8 @@ export const WorkflowDiagramFilterEdgeEditable = ({
>
<WorkflowDiagramEdgeV2VisibilityContainer shouldDisplay>
<StyledConfiguredFilterContainer>
{isEdgeHovered(id) || isDropdownOpen || isEdgeSelected ? (
<StyledIconButtonGroup
className="nodrag nopan"
{isEdgeHovered(id) || isDropdownOpen || nodeCreationStarted ? (
<WorkflowDiagramEdgeButtonGroup
iconButtons={[
{
Icon: IconFilter,
@@ -1,23 +1,13 @@
import { WORKFLOW_DIAGRAM_EDGE_OPTIONS_CLICK_OUTSIDE_ID } from '@/workflow/workflow-diagram/constants/WorkflowDiagramEdgeOptionsClickOutsideId';
import { useStartNodeCreation } from '@/workflow/workflow-diagram/hooks/useStartNodeCreation';
import { WorkflowDiagramEdge } from '@/workflow/workflow-diagram/types/WorkflowDiagram';
import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import {
BaseEdge,
EdgeLabelRenderer,
EdgeProps,
getBezierPath,
} from '@xyflow/react';
import { EdgeLabelRenderer, EdgeProps, getBezierPath } from '@xyflow/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;
`;
import { useEdgeHovered } from '@/workflow/workflow-diagram/hooks/useEdgeHovered';
import { WorkflowDiagramBaseEdge } from '@/workflow/workflow-diagram/components/WorkflowDiagramBaseEdge';
import { WorkflowDiagramEdgeButtonGroup } from '@/workflow/workflow-diagram/components/WorkflowDiagramEdgeButtonGroup';
type WorkflowDiagramFilteringDisabledEdgeEditableProps =
EdgeProps<WorkflowDiagramEdge>;
@@ -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 (
<>
<BaseEdge
<WorkflowDiagramBaseEdge
source={source}
target={target}
path={edgePath}
markerStart={markerStart}
markerEnd={markerEnd}
path={edgePath}
style={{ stroke: theme.border.color.strong }}
/>
<EdgeLabelRenderer>
@@ -75,16 +64,16 @@ export const WorkflowDiagramFilteringDisabledEdgeEditable = ({
labelY={labelY}
>
<WorkflowDiagramEdgeV2VisibilityContainer
shouldDisplay={isEdgeHovered(id) || forceDisplayAddButton}
shouldDisplay={isEdgeHovered(id) || nodeCreationStarted}
>
<StyledIconButtonGroup
className="nodrag nopan"
<WorkflowDiagramEdgeButtonGroup
iconButtons={[
{
Icon: IconPlus,
onClick: handleAddNodeButtonClick,
},
]}
selected={nodeCreationStarted}
/>
</WorkflowDiagramEdgeV2VisibilityContainer>
</WorkflowDiagramEdgeV2Container>
@@ -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<HTMLDivElement>) => void;
onMouseLeave?: (event: MouseEvent<HTMLDivElement>) => void;
}) => {
const { getNodeHandlesSelectedState } = useEdgeSelected();
const handlesSelectedState = getNodeHandlesSelectedState(id);
return (
<StyledStepNodeContainer
className="workflow-node-container"
@@ -173,6 +180,7 @@ export const WorkflowDiagramStepNodeBase = ({
type="target"
position={Position.Top}
isVisible={displayHandle}
selected={handlesSelectedState.targetHandle}
/>
)}
@@ -196,6 +204,7 @@ export const WorkflowDiagramStepNodeBase = ({
type="source"
position={Position.Bottom}
isVisible={displayHandle}
selected={handlesSelectedState.sourceHandle}
/>
</StyledStepNodeContainer>
);
@@ -30,6 +30,7 @@ export const WorkflowDiagramStepNodeEditable = ({
return (
<WorkflowDiagramStepNodeEditableContent
id={id}
data={data}
variant="default"
selected={selected ?? false}
@@ -34,11 +34,13 @@ const StyledAddStepButtonContainer = styled.div<{
`;
export const WorkflowDiagramStepNodeEditableContent = ({
id,
data,
selected,
variant,
onDelete,
}: {
id: string;
data: WorkflowDiagramStepNodeData;
variant: WorkflowDiagramNodeVariant;
selected: boolean;
@@ -58,6 +60,7 @@ export const WorkflowDiagramStepNodeEditableContent = ({
<WorkflowDiagramStepNodeBase
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
id={id}
name={data.name}
variant={variant}
nodeType={data.nodeType}
@@ -5,12 +5,15 @@ import { getNodeVariantFromStepRunStatus } from '@/workflow/workflow-diagram/uti
import React from 'react';
export const WorkflowDiagramStepNodeReadonly = ({
id,
data,
}: {
id: string;
data: WorkflowDiagramStepNodeData;
}) => {
return (
<WorkflowDiagramStepNodeBase
id={id}
name={data.name}
variant={getNodeVariantFromStepRunStatus(data.runStatus)}
nodeType={data.nodeType}
@@ -4,6 +4,8 @@ import '@xyflow/react/dist/style.css';
import { ReactflowDecorator } from '~/testing/decorators/ReactflowDecorator';
import { WorkflowDiagramEmptyTrigger } from '../WorkflowDiagramEmptyTrigger';
import { ComponentDecorator } from 'twenty-ui/testing';
import { RecoilRoot } from 'recoil';
import { WorkflowVisualizerComponentInstanceContext } from '@/workflow/workflow-diagram/states/contexts/WorkflowVisualizerComponentInstanceContext';
const meta: Meta<typeof WorkflowDiagramEmptyTrigger> = {
title: 'Modules/Workflow/WorkflowDiagramEmptyTrigger',
@@ -22,7 +24,13 @@ export const Default: Story = {
decorators: [
(Story) => (
<div style={{ position: 'relative' }}>
<Story />
<RecoilRoot>
<WorkflowVisualizerComponentInstanceContext.Provider
value={{ instanceId: 'workflow-visualizer-instance-id' }}
>
<Story />
</WorkflowVisualizerComponentInstanceContext.Provider>
</RecoilRoot>
</div>
),
ReactflowDecorator,
@@ -34,7 +42,13 @@ export const Selected: Story = {
decorators: [
(Story) => (
<div className="selectable selected" style={{ position: 'relative' }}>
<Story />
<RecoilRoot>
<WorkflowVisualizerComponentInstanceContext.Provider
value={{ instanceId: 'workflow-visualizer-instance-id' }}
>
<Story />
</WorkflowVisualizerComponentInstanceContext.Provider>
</RecoilRoot>
</div>
),
ReactflowDecorator,
@@ -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,
};
};
@@ -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,
};
};
@@ -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,
});
@@ -0,0 +1,4 @@
export type WorkflowDiagramEdge = {
source: string;
target: string;
};
@@ -0,0 +1,4 @@
export type WorkflowDiagramHandlesSelected = {
targetHandle: boolean;
sourceHandle: boolean;
};
@@ -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',
@@ -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',
@@ -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;
}
@@ -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;
}