Improve edge path algorithm (#14600)
https://github.com/user-attachments/assets/795b8293-60d2-44de-b261-7db093716e14 Closes https://github.com/twentyhq/core-team-issues/issues/1470
This commit is contained in:
committed by
GitHub
parent
80fec01707
commit
4d994fc566
@@ -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;
|
||||
|
||||
+9
@@ -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,
|
||||
|
||||
+117
@@ -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);
|
||||
});
|
||||
});
|
||||
+28
-14
@@ -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,
|
||||
|
||||
+37
-23
@@ -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,
|
||||
|
||||
+2
@@ -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,
|
||||
|
||||
+30
@@ -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;
|
||||
};
|
||||
+20
-8
@@ -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 }}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
+25
-11
@@ -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) && (
|
||||
|
||||
+22
-8
@@ -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 }}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
+20
-7
@@ -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}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
+122
@@ -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],
|
||||
};
|
||||
};
|
||||
Reference in New Issue
Block a user