Baptiste Devessier
2025-09-18 17:43:42 +02:00
committed by GitHub
parent 80fec01707
commit 4d994fc566
13 changed files with 488 additions and 71 deletions
@@ -104,10 +104,15 @@ export type WorkflowDiagramEdgeLabelOptions = {
label: MessageDescriptor;
};
export type WorkflowDiagramEdgePathStrategy =
| 'smooth-step-path-to-target'
| 'bypass-source-node-on-right-side';
export type WorkflowDiagramDefaultEdgeData = {
edgeType: 'default';
edgeExecutionStatus?: StepStatus;
labelOptions?: WorkflowDiagramEdgeLabelOptions;
edgePathStrategy?: WorkflowDiagramEdgePathStrategy;
};
export type WorkflowDiagramEdgeData = WorkflowDiagramDefaultEdgeData;
@@ -130,6 +130,7 @@ describe('generateWorkflowRunDiagram', () => {
{
"data": {
"edgeExecutionStatus": "FAILED",
"edgePathStrategy": undefined,
"edgeType": "default",
},
"deletable": false,
@@ -147,6 +148,7 @@ describe('generateWorkflowRunDiagram', () => {
{
"data": {
"edgeExecutionStatus": "NOT_STARTED",
"edgePathStrategy": undefined,
"edgeType": "default",
},
"deletable": false,
@@ -367,6 +369,7 @@ describe('generateWorkflowRunDiagram', () => {
{
"data": {
"edgeExecutionStatus": "SUCCESS",
"edgePathStrategy": undefined,
"edgeType": "default",
},
"deletable": false,
@@ -384,6 +387,7 @@ describe('generateWorkflowRunDiagram', () => {
{
"data": {
"edgeExecutionStatus": "SUCCESS",
"edgePathStrategy": undefined,
"edgeType": "default",
},
"deletable": false,
@@ -604,6 +608,7 @@ describe('generateWorkflowRunDiagram', () => {
{
"data": {
"edgeExecutionStatus": "RUNNING",
"edgePathStrategy": undefined,
"edgeType": "default",
},
"deletable": false,
@@ -621,6 +626,7 @@ describe('generateWorkflowRunDiagram', () => {
{
"data": {
"edgeExecutionStatus": "NOT_STARTED",
"edgePathStrategy": undefined,
"edgeType": "default",
},
"deletable": false,
@@ -860,6 +866,7 @@ describe('generateWorkflowRunDiagram', () => {
{
"data": {
"edgeExecutionStatus": "SUCCESS",
"edgePathStrategy": undefined,
"edgeType": "default",
},
"deletable": false,
@@ -877,6 +884,7 @@ describe('generateWorkflowRunDiagram', () => {
{
"data": {
"edgeExecutionStatus": "RUNNING",
"edgePathStrategy": undefined,
"edgeType": "default",
},
"deletable": false,
@@ -894,6 +902,7 @@ describe('generateWorkflowRunDiagram', () => {
{
"data": {
"edgeExecutionStatus": "NOT_STARTED",
"edgePathStrategy": undefined,
"edgeType": "default",
},
"deletable": false,
@@ -0,0 +1,117 @@
import {
type WorkflowIteratorAction,
type WorkflowStep,
} from '@/workflow/types/Workflow';
import { isLastStepOfLoop } from '../isLastStepOfLoop';
describe('isLastStepOfLoop', () => {
const makeIterator = (
id: string,
initialLoopStepIds: string[],
): WorkflowIteratorAction => ({
id,
type: 'ITERATOR',
settings: {
input: { initialLoopStepIds },
outputSchema: {},
errorHandlingOptions: {
retryOnFailure: { value: false },
continueOnFailure: { value: false },
},
},
name: 'Iterator',
valid: true,
});
const makeStep = (id: string, nextStepIds: string[] = []): WorkflowStep =>
({
id,
type: 'CODE',
nextStepIds,
name: id,
valid: true,
settings: {},
}) as WorkflowStep;
it('returns true when step is last before looping back to iterator', () => {
const iterator = makeIterator('iterator', ['A']);
const steps = [
makeStep('A', ['B']),
makeStep('B', ['iterator']),
makeStep('iterator', []),
];
expect(isLastStepOfLoop({ iterator, stepId: 'B', steps })).toBe(true);
});
it('returns false when step is not last before looping back', () => {
const iterator = makeIterator('iterator', ['A']);
const steps = [
makeStep('A', ['B']),
makeStep('B', ['C']),
makeStep('C', ['iterator']),
makeStep('iterator', []),
];
expect(isLastStepOfLoop({ iterator, stepId: 'B', steps })).toBe(false);
});
it('returns false if step is not in loop', () => {
const iterator = makeIterator('iterator', ['A']);
const steps = [
makeStep('A', ['B']),
makeStep('B', ['iterator']),
makeStep('iterator', []),
];
expect(isLastStepOfLoop({ iterator, stepId: 'X', steps })).toBe(false);
});
it('returns false if loop never reaches step', () => {
const iterator = makeIterator('iterator', ['A']);
const steps = [makeStep('A', ['iterator']), makeStep('iterator', [])];
expect(isLastStepOfLoop({ iterator, stepId: 'B', steps })).toBe(false);
});
it('returns false if step does not point to iterator', () => {
const iterator = makeIterator('iterator', ['A']);
const steps = [
makeStep('A', ['B']),
makeStep('B', ['C']),
makeStep('C', ['iterator']),
makeStep('iterator', []),
];
expect(isLastStepOfLoop({ iterator, stepId: 'B', steps })).toBe(false);
});
it('handles cycles and does not infinite loop, still finds last step', () => {
const iterator = makeIterator('iterator', ['A']);
const steps = [
makeStep('A', ['B']),
makeStep('B', ['A', 'iterator']),
makeStep('iterator', []),
];
expect(isLastStepOfLoop({ iterator, stepId: 'B', steps })).toBe(true);
});
it('continues processing even if revisiting iterator node', () => {
const iterator = makeIterator('iterator', ['A']);
const steps = [
makeStep('A', ['iterator', 'B']),
makeStep('B', ['iterator']),
makeStep('iterator', ['A']),
];
// B is reachable after revisiting iterator, and points to iterator
expect(isLastStepOfLoop({ iterator, stepId: 'B', steps })).toBe(true);
});
it('returns true if multiple initialLoopStepIds and one is last', () => {
const iterator = makeIterator('iterator', ['A', 'X']);
const steps = [
makeStep('A', ['B']),
makeStep('B', ['iterator']),
makeStep('X', ['Y']),
makeStep('Y', ['iterator']),
makeStep('iterator', []),
];
expect(isLastStepOfLoop({ iterator, stepId: 'B', steps })).toBe(true);
expect(isLastStepOfLoop({ iterator, stepId: 'Y', steps })).toBe(true);
});
});
@@ -5,15 +5,18 @@ import {
type WorkflowDiagramNode,
type WorkflowDiagramStepNodeData,
} from '@/workflow/workflow-diagram/types/WorkflowDiagram';
import { getEdgePathStrategy } from '@/workflow/workflow-diagram/utils/getEdgePathStrategy';
import { getEdgeTypeBetweenTwoNodes } from '@/workflow/workflow-diagram/utils/getEdgeTypeBetweenTwoNodes';
import { WORKFLOW_VISUALIZER_EDGE_DEFAULT_CONFIGURATION } from '@/workflow/workflow-diagram/workflow-edges/constants/WorkflowVisualizerEdgeDefaultConfiguration';
import { WORKFLOW_DIAGRAM_NODE_DEFAULT_SOURCE_HANDLE_ID } from '@/workflow/workflow-diagram/workflow-nodes/constants/WorkflowDiagramNodeDefaultSourceHandleId';
import { WORKFLOW_DIAGRAM_NODE_DEFAULT_TARGET_HANDLE_ID } from '@/workflow/workflow-diagram/workflow-nodes/constants/WorkflowDiagramNodeDefaultTargetHandleId';
import { isNonEmptyArray } from '@sniptt/guards';
import { isDefined } from 'twenty-shared/utils';
import { v4 } from 'uuid';
export const generateNodesAndEdgesForDefaultNode = ({
step,
steps,
xPos,
yPos,
nodes,
@@ -21,6 +24,7 @@ export const generateNodesAndEdgesForDefaultNode = ({
workflowContext,
}: {
step: WorkflowStep;
steps: WorkflowStep[];
yPos: number;
xPos: number;
nodes: readonly WorkflowDiagramNode[];
@@ -57,20 +61,30 @@ export const generateNodesAndEdgesForDefaultNode = ({
},
});
step.nextStepIds?.forEach((child) => {
updatedEdges.push({
...WORKFLOW_VISUALIZER_EDGE_DEFAULT_CONFIGURATION,
type: edgeTypeBetweenTwoNodes,
id: v4(),
source: step.id,
sourceHandle: WORKFLOW_DIAGRAM_NODE_DEFAULT_SOURCE_HANDLE_ID,
target: child,
targetHandle: WORKFLOW_DIAGRAM_NODE_DEFAULT_TARGET_HANDLE_ID,
...(edgeTypeBetweenTwoNodes.includes('editable')
? { deletable: true, selectable: true }
: {}),
});
});
if (isNonEmptyArray(step.nextStepIds)) {
for (const nextStepId of step.nextStepIds) {
updatedEdges.push({
...WORKFLOW_VISUALIZER_EDGE_DEFAULT_CONFIGURATION,
type: edgeTypeBetweenTwoNodes,
id: v4(),
source: step.id,
sourceHandle: WORKFLOW_DIAGRAM_NODE_DEFAULT_SOURCE_HANDLE_ID,
target: nextStepId,
targetHandle: WORKFLOW_DIAGRAM_NODE_DEFAULT_TARGET_HANDLE_ID,
data: {
...WORKFLOW_VISUALIZER_EDGE_DEFAULT_CONFIGURATION.data,
edgePathStrategy: getEdgePathStrategy({
step,
steps,
nextStepId,
}),
},
...(edgeTypeBetweenTwoNodes.includes('editable')
? { deletable: true, selectable: true }
: {}),
});
}
}
return {
nodes: updatedNodes,
@@ -1,30 +1,36 @@
import { type WorkflowStep } from '@/workflow/types/Workflow';
import {
type WorkflowIteratorAction,
type WorkflowStep,
} from '@/workflow/types/Workflow';
import { type WorkflowContext } from '@/workflow/workflow-diagram/types/WorkflowContext';
import {
type WorkflowDiagramEdge,
type WorkflowDiagramNode,
type WorkflowDiagramStepNodeData,
} from '@/workflow/workflow-diagram/types/WorkflowDiagram';
import { getEdgePathStrategy } from '@/workflow/workflow-diagram/utils/getEdgePathStrategy';
import { getEdgeTypeBetweenTwoNodes } from '@/workflow/workflow-diagram/utils/getEdgeTypeBetweenTwoNodes';
import { WORKFLOW_VISUALIZER_EDGE_DEFAULT_CONFIGURATION } from '@/workflow/workflow-diagram/workflow-edges/constants/WorkflowVisualizerEdgeDefaultConfiguration';
import { WORKFLOW_DIAGRAM_ITERATOR_NODE_LOOP_HANDLE_ID } from '@/workflow/workflow-diagram/workflow-nodes/constants/WorkflowDiagramIteratorNodeLoopHandleId';
import { WORKFLOW_DIAGRAM_NODE_DEFAULT_SOURCE_HANDLE_ID } from '@/workflow/workflow-diagram/workflow-nodes/constants/WorkflowDiagramNodeDefaultSourceHandleId';
import { WORKFLOW_DIAGRAM_NODE_DEFAULT_TARGET_HANDLE_ID } from '@/workflow/workflow-diagram/workflow-nodes/constants/WorkflowDiagramNodeDefaultTargetHandleId';
import { msg } from '@lingui/core/macro';
import { isNonEmptyString } from '@sniptt/guards';
import { isNonEmptyArray, isNonEmptyString } from '@sniptt/guards';
import { Position } from '@xyflow/react';
import { isDefined } from 'twenty-shared/utils';
import { v4 } from 'uuid';
export const generateNodesAndEdgesForIteratorNode = ({
step,
steps,
xPos,
yPos,
nodes,
edges,
workflowContext,
}: {
step: WorkflowStep & { type: 'ITERATOR' };
step: WorkflowIteratorAction;
steps: WorkflowStep[];
yPos: number;
xPos: number;
nodes: readonly WorkflowDiagramNode[];
@@ -95,31 +101,39 @@ export const generateNodesAndEdgesForIteratorNode = ({
position: Position.Right,
label: msg`loop`,
},
edgePathStrategy: 'smooth-step-path-to-target',
},
});
}
step.nextStepIds?.forEach((child) => {
updatedEdges.push({
...WORKFLOW_VISUALIZER_EDGE_DEFAULT_CONFIGURATION,
type: edgeTypeBetweenTwoNodes,
id: v4(),
source: step.id,
sourceHandle: WORKFLOW_DIAGRAM_NODE_DEFAULT_SOURCE_HANDLE_ID,
target: child,
...(edgeTypeBetweenTwoNodes.includes('editable')
? { deletable: true, selectable: true }
: {}),
targetHandle: WORKFLOW_DIAGRAM_NODE_DEFAULT_TARGET_HANDLE_ID,
data: {
...WORKFLOW_VISUALIZER_EDGE_DEFAULT_CONFIGURATION.data,
labelOptions: {
position: Position.Bottom,
label: msg`completed`,
if (isNonEmptyArray(step.nextStepIds)) {
for (const nextStepId of step.nextStepIds) {
updatedEdges.push({
...WORKFLOW_VISUALIZER_EDGE_DEFAULT_CONFIGURATION,
type: edgeTypeBetweenTwoNodes,
id: v4(),
source: step.id,
sourceHandle: WORKFLOW_DIAGRAM_NODE_DEFAULT_SOURCE_HANDLE_ID,
target: nextStepId,
...(edgeTypeBetweenTwoNodes.includes('editable')
? { deletable: true, selectable: true }
: {}),
targetHandle: WORKFLOW_DIAGRAM_NODE_DEFAULT_TARGET_HANDLE_ID,
data: {
...WORKFLOW_VISUALIZER_EDGE_DEFAULT_CONFIGURATION.data,
labelOptions: {
position: Position.Bottom,
label: msg`completed`,
},
edgePathStrategy: getEdgePathStrategy({
step,
steps,
nextStepId,
}),
},
},
});
});
});
}
}
return {
nodes: updatedNodes,
@@ -70,6 +70,7 @@ export const generateWorkflowDiagram = ({
const { nodes: iteratorNodes, edges: iteratorEdges } =
generateNodesAndEdgesForIteratorNode({
step,
steps,
xPos,
yPos: levelYPos,
nodes,
@@ -86,6 +87,7 @@ export const generateWorkflowDiagram = ({
const { nodes: defaultNodes, edges: defaultEdges } =
generateNodesAndEdgesForDefaultNode({
step,
steps,
xPos,
yPos: levelYPos,
nodes,
@@ -0,0 +1,30 @@
import { type WorkflowStep } from '@/workflow/types/Workflow';
import { isLastStepOfLoop } from '@/workflow/workflow-diagram/utils/isLastStepOfLoop';
import { isDefined } from 'twenty-shared/utils';
export const getEdgePathStrategy = ({
step,
nextStepId,
steps,
}: {
step: WorkflowStep;
nextStepId: string;
steps: WorkflowStep[];
}) => {
const nextStep = steps.find((s) => s.id === nextStepId);
if (!isDefined(nextStep)) {
throw new Error('Expected to find step defined in nextStepIds');
}
const useLoopBackToIteratorStyle =
nextStep.type === 'ITERATOR' &&
isLastStepOfLoop({
iterator: nextStep,
stepId: step.id,
steps,
});
return useLoopBackToIteratorStyle
? 'bypass-source-node-on-right-side'
: undefined;
};
@@ -0,0 +1,51 @@
import {
type WorkflowIteratorAction,
type WorkflowStep,
} from '@/workflow/types/Workflow';
import { isNonEmptyArray } from '@sniptt/guards';
import { isDefined } from 'twenty-shared/utils';
export const isLastStepOfLoop = ({
iterator,
stepId,
steps,
}: {
iterator: WorkflowIteratorAction;
stepId: string;
steps: WorkflowStep[];
}): boolean => {
const stepMap = new Map<string, WorkflowStep>(steps.map((s) => [s.id, s]));
const visited = new Set<string>();
const queue = [...(iterator.settings.input?.initialLoopStepIds ?? [])];
while (queue.length > 0) {
const currentId = queue.shift();
if (!isDefined(currentId) || visited.has(currentId)) {
continue;
}
visited.add(currentId);
const currentStep = stepMap.get(currentId);
if (!isDefined(currentStep)) {
continue;
}
if (
currentId === stepId &&
currentStep.nextStepIds?.includes(iterator.id) === true
) {
return true;
}
if (isNonEmptyArray(currentStep.nextStepIds)) {
for (const nextId of currentStep.nextStepIds) {
queue.push(nextId);
}
}
}
return false;
};
@@ -1,6 +1,7 @@
import { type WorkflowDiagramEdge } from '@/workflow/workflow-diagram/types/WorkflowDiagram';
import { getEdgePath } from '@/workflow/workflow-diagram/workflow-edges/utils/getEdgePath';
import { useTheme } from '@emotion/react';
import { BaseEdge, type EdgeProps, getBezierPath } from '@xyflow/react';
import { BaseEdge, type EdgeProps } from '@xyflow/react';
type WorkflowDiagramBlankEdgeProps = EdgeProps<WorkflowDiagramEdge>;
@@ -9,24 +10,35 @@ export const WorkflowDiagramBlankEdge = ({
markerEnd,
sourceY,
sourceX,
sourcePosition,
targetY,
targetX,
targetPosition,
}: WorkflowDiagramBlankEdgeProps) => {
const theme = useTheme();
const [edgePath] = getBezierPath({
const { segments } = getEdgePath({
sourceX,
sourceY,
sourcePosition,
targetX,
targetY,
targetPosition,
markerStart,
markerEnd,
});
return (
<BaseEdge
markerStart={markerStart}
markerEnd={markerEnd}
path={edgePath}
style={{ stroke: theme.border.color.strong }}
/>
<>
{segments.map((segment) => (
<BaseEdge
key={segment.path}
markerStart={segment.markerStart}
markerEnd={segment.markerEnd}
path={segment.path}
style={{ stroke: theme.border.color.strong }}
/>
))}
</>
);
};
@@ -9,9 +9,10 @@ import { WORKFLOW_DIAGRAM_EDGE_OPTIONS_CLICK_OUTSIDE_ID } from '@/workflow/workf
import { useEdgeState } from '@/workflow/workflow-diagram/workflow-edges/hooks/useEdgeState';
import { type WorkflowDiagramEdgeComponentProps } from '@/workflow/workflow-diagram/workflow-edges/types/WorkflowDiagramEdgeComponentProps';
import { getConnectionOptionsForSourceHandle } from '@/workflow/workflow-diagram/workflow-edges/utils/getConnectionOptionsForSourceHandle';
import { getEdgePath } from '@/workflow/workflow-diagram/workflow-edges/utils/getEdgePath';
import { useDeleteEdge } from '@/workflow/workflow-steps/hooks/useDeleteEdge';
import { useLingui } from '@lingui/react/macro';
import { EdgeLabelRenderer, getBezierPath } from '@xyflow/react';
import { EdgeLabelRenderer } from '@xyflow/react';
import { type MouseEvent } from 'react';
import { isDefined } from 'twenty-shared/utils';
import { IconPlus, IconTrash } from 'twenty-ui/display';
@@ -22,8 +23,10 @@ type WorkflowDiagramDefaultEdgeEditableProps =
export const WorkflowDiagramDefaultEdgeEditable = ({
source,
sourceHandleId,
sourcePosition,
target,
targetHandleId,
targetPosition,
sourceX,
sourceY,
targetX,
@@ -36,11 +39,19 @@ export const WorkflowDiagramDefaultEdgeEditable = ({
const { isEdgeHovered } = useEdgeState();
const [edgePath, labelX, labelY] = getBezierPath({
const {
segments,
labelPosition: [labelX, labelY],
} = getEdgePath({
sourceX,
sourceY,
sourcePosition,
targetX,
targetY,
targetPosition,
markerStart,
markerEnd,
strategy: data?.edgePathStrategy,
});
const { deleteEdge } = useDeleteEdge();
@@ -77,15 +88,18 @@ export const WorkflowDiagramDefaultEdgeEditable = ({
return (
<>
<WorkflowDiagramBaseEdge
source={source}
sourceHandleId={sourceHandleId}
target={target}
targetHandleId={targetHandleId}
path={edgePath}
markerStart={markerStart}
markerEnd={markerEnd}
/>
{segments.map((segment) => (
<WorkflowDiagramBaseEdge
key={segment.path}
source={source}
sourceHandleId={sourceHandleId}
target={target}
targetHandleId={targetHandleId}
path={segment.path}
markerStart={segment.markerStart}
markerEnd={segment.markerEnd}
/>
))}
<EdgeLabelRenderer>
{isDefined(data?.labelOptions) && (
@@ -1,6 +1,7 @@
import { type WorkflowDiagramEdgeComponentProps } from '@/workflow/workflow-diagram/workflow-edges/types/WorkflowDiagramEdgeComponentProps';
import { getEdgePath } from '@/workflow/workflow-diagram/workflow-edges/utils/getEdgePath';
import { useTheme } from '@emotion/react';
import { BaseEdge, getBezierPath } from '@xyflow/react';
import { BaseEdge } from '@xyflow/react';
type WorkflowDiagramDefaultEdgeReadonlyProps =
WorkflowDiagramEdgeComponentProps;
@@ -8,26 +9,39 @@ type WorkflowDiagramDefaultEdgeReadonlyProps =
export const WorkflowDiagramDefaultEdgeReadonly = ({
sourceX,
sourceY,
sourcePosition,
targetX,
targetY,
targetPosition,
markerStart,
markerEnd,
data,
}: WorkflowDiagramDefaultEdgeReadonlyProps) => {
const theme = useTheme();
const [edgePath] = getBezierPath({
const { segments } = getEdgePath({
sourceX,
sourceY,
sourcePosition,
targetX,
targetY,
targetPosition,
markerStart,
markerEnd,
strategy: data?.edgePathStrategy,
});
return (
<BaseEdge
markerStart={markerStart}
markerEnd={markerEnd}
path={edgePath}
style={{ stroke: theme.border.color.strong }}
/>
<>
{segments.map((segment) => (
<BaseEdge
key={segment.path}
markerStart={segment.markerStart}
markerEnd={segment.markerEnd}
path={segment.path}
style={{ stroke: theme.border.color.strong }}
/>
))}
</>
);
};
@@ -1,29 +1,42 @@
import { WorkflowRunDiagramBaseEdge } from '@/workflow/workflow-diagram/workflow-edges/components/WorkflowRunDiagramBaseEdge';
import { type WorkflowDiagramEdgeComponentProps } from '@/workflow/workflow-diagram/workflow-edges/types/WorkflowDiagramEdgeComponentProps';
import { getBezierPath } from '@xyflow/react';
import { getEdgePath } from '@/workflow/workflow-diagram/workflow-edges/utils/getEdgePath';
type WorkflowDiagramDefaultEdgeRunProps = WorkflowDiagramEdgeComponentProps;
export const WorkflowDiagramDefaultEdgeRun = ({
sourceX,
sourceY,
sourcePosition,
targetX,
targetY,
targetPosition,
markerStart,
markerEnd,
data,
}: WorkflowDiagramDefaultEdgeRunProps) => {
const [edgePath] = getBezierPath({
const { segments } = getEdgePath({
sourceX,
sourceY,
sourcePosition,
targetX,
targetY,
targetPosition,
markerStart,
markerEnd,
strategy: data?.edgePathStrategy,
});
return (
<WorkflowRunDiagramBaseEdge
path={edgePath}
markerStart={markerStart}
markerEnd={markerEnd}
/>
<>
{segments.map((segment) => (
<WorkflowRunDiagramBaseEdge
key={segment.path}
path={segment.path}
markerStart={segment.markerStart}
markerEnd={segment.markerEnd}
/>
))}
</>
);
};
@@ -0,0 +1,122 @@
import { type WorkflowDiagramEdgePathStrategy } from '@/workflow/workflow-diagram/types/WorkflowDiagram';
import { type WorkflowDiagramEdgeComponentProps } from '@/workflow/workflow-diagram/workflow-edges/types/WorkflowDiagramEdgeComponentProps';
import { getBezierPath, getSmoothStepPath, Position } from '@xyflow/react';
const EDGE_PADDING_BOTTOM = 40;
const EDGE_PADDING_X = 40;
const EDGE_BORDER_RADIUS = 16;
type GetEdgePathParams = Pick<
WorkflowDiagramEdgeComponentProps,
| 'sourceX'
| 'sourceY'
| 'sourcePosition'
| 'targetX'
| 'targetY'
| 'targetPosition'
| 'markerStart'
| 'markerEnd'
> & {
strategy?: WorkflowDiagramEdgePathStrategy;
};
export const getEdgePath = ({
sourceX,
sourceY,
sourcePosition,
targetX,
targetY,
targetPosition,
markerStart,
markerEnd,
strategy,
}: GetEdgePathParams) => {
if (strategy === 'smooth-step-path-to-target') {
const [path, labelX, labelY] = getSmoothStepPath({
sourceX,
sourceY,
sourcePosition,
targetX,
targetY,
targetPosition,
borderRadius: EDGE_BORDER_RADIUS,
offset: EDGE_PADDING_X,
});
return {
segments: [
{
path,
markerStart,
markerEnd,
},
],
labelPosition: [labelX, labelY],
};
}
if (sourceY < targetY) {
const [path, labelX, labelY] = getBezierPath({
sourceX,
sourceY,
sourcePosition,
targetX,
targetY,
targetPosition,
});
return {
segments: [
{
path,
markerStart,
markerEnd,
},
],
labelPosition: [labelX, labelY],
};
}
const firstSegmentTargetX =
strategy === 'bypass-source-node-on-right-side'
? sourceX + 200
: (sourceX + targetX) / 2;
const firstSegmentTargetY = sourceY + EDGE_PADDING_BOTTOM;
const firstSegment = getSmoothStepPath({
sourceX,
sourceY,
targetX: firstSegmentTargetX,
targetY: firstSegmentTargetY,
sourcePosition,
targetPosition: Position.Bottom,
borderRadius: EDGE_BORDER_RADIUS,
offset: EDGE_PADDING_X,
});
const secondSegment = getSmoothStepPath({
sourceX: firstSegmentTargetX,
sourceY: firstSegmentTargetY,
targetX,
targetY,
sourcePosition: Position.Top,
targetPosition,
borderRadius: EDGE_BORDER_RADIUS,
offset: EDGE_PADDING_X,
});
return {
segments: [
{
path: firstSegment[0],
markerStart,
markerEnd: undefined,
},
{
path: secondSegment[0],
markerStart: undefined,
markerEnd,
},
],
labelPosition: [firstSegmentTargetX, firstSegmentTargetY],
};
};