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:
+38
@@ -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,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
+14
-6
@@ -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}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
+8
-5
@@ -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,
|
||||
|
||||
+39
-1
@@ -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,
|
||||
});
|
||||
|
||||
+17
-32
@@ -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>
|
||||
|
||||
+34
@@ -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}
|
||||
/>
|
||||
);
|
||||
};
|
||||
+1
@@ -13,6 +13,7 @@ const StyledStepNodeLabelIconContainer = styled.div`
|
||||
export const WorkflowDiagramEmptyTrigger = () => {
|
||||
return (
|
||||
<WorkflowDiagramStepNodeBase
|
||||
id={'trigger'}
|
||||
name="Add a Trigger"
|
||||
nodeType="trigger"
|
||||
variant="empty"
|
||||
|
||||
+13
-18
@@ -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,
|
||||
|
||||
+13
-24
@@ -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>
|
||||
|
||||
+9
@@ -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>
|
||||
);
|
||||
|
||||
+1
@@ -30,6 +30,7 @@ export const WorkflowDiagramStepNodeEditable = ({
|
||||
|
||||
return (
|
||||
<WorkflowDiagramStepNodeEditableContent
|
||||
id={id}
|
||||
data={data}
|
||||
variant="default"
|
||||
selected={selected ?? false}
|
||||
|
||||
+3
@@ -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}
|
||||
|
||||
+3
@@ -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}
|
||||
|
||||
+16
-2
@@ -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,
|
||||
|
||||
+6
-2
@@ -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,
|
||||
};
|
||||
};
|
||||
+11
@@ -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,
|
||||
});
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
export type WorkflowDiagramEdge = {
|
||||
source: string;
|
||||
target: string;
|
||||
};
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
export type WorkflowDiagramHandlesSelected = {
|
||||
targetHandle: boolean;
|
||||
sourceHandle: boolean;
|
||||
};
|
||||
+4
@@ -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',
|
||||
|
||||
+1
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user