diff --git a/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowDiagramCanvasEditable.tsx b/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowDiagramCanvasEditable.tsx
index 66e04b7ad9..d2d7ba3625 100644
--- a/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowDiagramCanvasEditable.tsx
+++ b/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowDiagramCanvasEditable.tsx
@@ -2,10 +2,8 @@ import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/ho
import { useSetRecoilComponentState } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentState';
import { useWorkflowWithCurrentVersion } from '@/workflow/hooks/useWorkflowWithCurrentVersion';
import { workflowVisualizerWorkflowIdComponentState } from '@/workflow/states/workflowVisualizerWorkflowIdComponentState';
-import { WorkflowDiagramBlankEdge } from '@/workflow/workflow-diagram/workflow-edges/components/WorkflowDiagramBlankEdge';
import { WorkflowDiagramCanvasBase } from '@/workflow/workflow-diagram/components/WorkflowDiagramCanvasBase';
import { WorkflowDiagramCanvasEditableEffect } from '@/workflow/workflow-diagram/components/WorkflowDiagramCanvasEditableEffect';
-import { WorkflowDiagramDefaultEdgeEditable } from '@/workflow/workflow-diagram/workflow-edges/components/WorkflowDiagramDefaultEdgeEditable';
import { workflowDiagramComponentState } from '@/workflow/workflow-diagram/states/workflowDiagramComponentState';
import { workflowDiagramRightClickMenuPositionState } from '@/workflow/workflow-diagram/states/workflowDiagramRightClickMenuPositionState';
import {
@@ -13,8 +11,11 @@ import {
type WorkflowDiagramNode,
} from '@/workflow/workflow-diagram/types/WorkflowDiagram';
import { getWorkflowVersionStatusTagProps } from '@/workflow/workflow-diagram/utils/getWorkflowVersionStatusTagProps';
-import { WorkflowDiagramEmptyTriggerEditable } from '@/workflow/workflow-diagram/workflow-nodes/components/WorkflowDiagramEmptyTriggerEditable';
+import { WorkflowDiagramBlankEdge } from '@/workflow/workflow-diagram/workflow-edges/components/WorkflowDiagramBlankEdge';
+import { WorkflowDiagramDefaultEdgeEditable } from '@/workflow/workflow-diagram/workflow-edges/components/WorkflowDiagramDefaultEdgeEditable';
import { WorkflowDiagramFilterEdgeEditable } from '@/workflow/workflow-diagram/workflow-edges/components/WorkflowDiagramFilterEdgeEditable';
+import { WorkflowDiagramEmptyTriggerEditable } from '@/workflow/workflow-diagram/workflow-nodes/components/WorkflowDiagramEmptyTriggerEditable';
+import { WorkflowDiagramIteratorEmptyActionEditable } from '@/workflow/workflow-diagram/workflow-nodes/components/WorkflowDiagramIteratorEmptyActionEditable';
import { WorkflowDiagramStepNodeEditable } from '@/workflow/workflow-diagram/workflow-nodes/components/WorkflowDiagramStepNodeEditable';
import { useCreateEdge } from '@/workflow/workflow-steps/hooks/useCreateEdge';
import { useDeleteEdge } from '@/workflow/workflow-steps/hooks/useDeleteEdge';
@@ -121,6 +122,7 @@ export const WorkflowDiagramCanvasEditable = () => {
nodeTypes={{
default: WorkflowDiagramStepNodeEditable,
'empty-trigger': WorkflowDiagramEmptyTriggerEditable,
+ 'iterator-empty-action': WorkflowDiagramIteratorEmptyActionEditable,
}}
edgeTypes={{
blank: WorkflowDiagramBlankEdge,
diff --git a/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowDiagramCreateStepElement.tsx b/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowDiagramCreateStepElement.tsx
index d0ca51220d..60205f14fa 100644
--- a/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowDiagramCreateStepElement.tsx
+++ b/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowDiagramCreateStepElement.tsx
@@ -19,12 +19,19 @@ const StyledVerticalLineContainer = styled.div`
width: 56px;
`;
+const StyledLabelContainer = styled.div`
+ position: absolute;
+ top: 14px;
+`;
+
type WorkflowDiagramCreateStepElementProps = {
data: WorkflowDiagramStepNodeData;
+ Label?: React.ReactNode;
};
export const WorkflowDiagramCreateStepElement = ({
data,
+ Label,
}: WorkflowDiagramCreateStepElementProps) => {
const { startNodeCreation } = useStartNodeCreation();
@@ -44,6 +51,9 @@ export const WorkflowDiagramCreateStepElement = ({
+
+ {Label && {Label}}
+
{
const nextWorkflowDiagram = getWorkflowVersionDiagram({
workflowVersion: currentVersion,
- isEditable: true,
+ workflowContext: 'workflow',
});
let mergedWorkflowDiagram = nextWorkflowDiagram;
diff --git a/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowVersionVisualizerEffect.tsx b/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowVersionVisualizerEffect.tsx
index f867a8a31e..77e8e4de1f 100644
--- a/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowVersionVisualizerEffect.tsx
+++ b/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowVersionVisualizerEffect.tsx
@@ -60,7 +60,7 @@ export const WorkflowVersionVisualizerEffect = ({
const nextWorkflowDiagram = getWorkflowVersionDiagram({
workflowVersion,
- isEditable: false,
+ workflowContext: 'workflow-version',
});
setWorkflowDiagram(nextWorkflowDiagram);
diff --git a/packages/twenty-front/src/modules/workflow/workflow-diagram/components/__stories__/WorkflowDiagramStepNodeEditableContent.stories.tsx b/packages/twenty-front/src/modules/workflow/workflow-diagram/components/__stories__/WorkflowDiagramStepNodeEditableContent.stories.tsx
index d4f37905a6..97406424e3 100644
--- a/packages/twenty-front/src/modules/workflow/workflow-diagram/components/__stories__/WorkflowDiagramStepNodeEditableContent.stories.tsx
+++ b/packages/twenty-front/src/modules/workflow/workflow-diagram/components/__stories__/WorkflowDiagramStepNodeEditableContent.stories.tsx
@@ -6,6 +6,7 @@ import { fn } from '@storybook/test';
import '@xyflow/react/dist/style.css';
import { RecoilRoot } from 'recoil';
import { CatalogDecorator, type CatalogStory } from 'twenty-ui/testing';
+import { I18nFrontDecorator } from '~/testing/decorators/I18nFrontDecorator';
import { ReactflowDecorator } from '~/testing/decorators/ReactflowDecorator';
import { graphqlMocks } from '~/testing/graphqlMocks';
import { WorkflowDiagramStepNodeEditableContent } from '../../workflow-nodes/components/WorkflowDiagramStepNodeEditableContent';
@@ -16,6 +17,7 @@ const meta: Meta = {
parameters: {
msw: graphqlMocks,
},
+ decorators: [I18nFrontDecorator],
};
export default meta;
diff --git a/packages/twenty-front/src/modules/workflow/workflow-diagram/types/WorkflowContext.ts b/packages/twenty-front/src/modules/workflow/workflow-diagram/types/WorkflowContext.ts
new file mode 100644
index 0000000000..c7fb2e4b64
--- /dev/null
+++ b/packages/twenty-front/src/modules/workflow/workflow-diagram/types/WorkflowContext.ts
@@ -0,0 +1 @@
+export type WorkflowContext = 'workflow' | 'workflow-version' | 'workflow-run';
diff --git a/packages/twenty-front/src/modules/workflow/workflow-diagram/types/WorkflowDiagram.ts b/packages/twenty-front/src/modules/workflow/workflow-diagram/types/WorkflowDiagram.ts
index a75c123304..ab09f3db3a 100644
--- a/packages/twenty-front/src/modules/workflow/workflow-diagram/types/WorkflowDiagram.ts
+++ b/packages/twenty-front/src/modules/workflow/workflow-diagram/types/WorkflowDiagram.ts
@@ -3,8 +3,10 @@ import {
type WorkflowRunStepStatus,
type WorkflowTriggerType,
} from '@/workflow/types/Workflow';
+import { type WorkflowDiagramIteratorEmptyActionNodeData } from '@/workflow/workflow-diagram/workflow-iterator/types/WorkflowDiagramIteratorEmptyActionNodeData';
import { type FilterSettings } from '@/workflow/workflow-steps/workflow-actions/filter-action/components/WorkflowEditActionFilter';
-import { type Edge, type Node } from '@xyflow/react';
+import { type MessageDescriptor } from '@lingui/core';
+import { type Edge, type Node, type Position } from '@xyflow/react';
import { type StepStatus } from 'twenty-shared/workflow';
export type WorkflowDiagramStepNode = Node;
@@ -23,6 +25,14 @@ export type WorkflowDiagram = {
edges: Array;
};
+export type WorkflowDiagramNodeRightHandleOptions = {
+ id: string;
+};
+
+export type WorkflowDiagramNodeDefaultHandleOptions = {
+ label?: MessageDescriptor;
+};
+
export type WorkflowDiagramStepNodeData =
| {
nodeType: 'trigger';
@@ -32,6 +42,8 @@ export type WorkflowDiagramStepNodeData =
runStatus?: WorkflowRunStepStatus;
hasNextStepIds: boolean;
stepId: string;
+ defaultHandleOptions?: WorkflowDiagramNodeDefaultHandleOptions;
+ rightHandleOptions?: WorkflowDiagramNodeRightHandleOptions;
position: {
x: number;
y: number;
@@ -44,6 +56,8 @@ export type WorkflowDiagramStepNodeData =
runStatus?: WorkflowRunStepStatus;
hasNextStepIds: boolean;
stepId: string;
+ defaultHandleOptions?: WorkflowDiagramNodeDefaultHandleOptions;
+ rightHandleOptions?: WorkflowDiagramNodeRightHandleOptions;
position: {
x: number;
y: number;
@@ -67,13 +81,19 @@ export type WorkflowDiagramEmptyTriggerNodeData = {
export type WorkflowDiagramNodeData =
| WorkflowDiagramStepNodeData
- | WorkflowDiagramEmptyTriggerNodeData;
+ | WorkflowDiagramEmptyTriggerNodeData
+ | WorkflowDiagramIteratorEmptyActionNodeData;
export type WorkflowRunDiagramNodeData = Exclude<
WorkflowDiagramStepNodeData,
'runStatus'
> & { runStatus: WorkflowRunStepStatus };
+export type WorkflowDiagramEdgeLabelOptions = {
+ position: Position;
+ label: MessageDescriptor;
+};
+
export type WorkflowDiagramFilterEdgeData = {
edgeType: 'filter';
stepId: string;
@@ -81,18 +101,23 @@ export type WorkflowDiagramFilterEdgeData = {
name: string;
runStatus?: WorkflowRunStepStatus;
edgeExecutionStatus?: StepStatus;
+ labelOptions?: WorkflowDiagramEdgeLabelOptions;
};
export type WorkflowDiagramDefaultEdgeData = {
edgeType: 'default';
edgeExecutionStatus?: StepStatus;
+ labelOptions?: WorkflowDiagramEdgeLabelOptions;
};
export type WorkflowDiagramEdgeData =
| WorkflowDiagramFilterEdgeData
| WorkflowDiagramDefaultEdgeData;
-export type WorkflowDiagramNodeType = 'default' | 'empty-trigger';
+export type WorkflowDiagramNodeType =
+ | 'default'
+ | 'empty-trigger'
+ | 'iterator-empty-action';
export type WorkflowDiagramEdgeType =
| 'blank'
diff --git a/packages/twenty-front/src/modules/workflow/workflow-diagram/utils/__tests__/generateWorkflowDiagram.test.ts b/packages/twenty-front/src/modules/workflow/workflow-diagram/utils/__tests__/generateWorkflowDiagram.test.ts
index 67fb41ab37..75380b19c3 100644
--- a/packages/twenty-front/src/modules/workflow/workflow-diagram/utils/__tests__/generateWorkflowDiagram.test.ts
+++ b/packages/twenty-front/src/modules/workflow/workflow-diagram/utils/__tests__/generateWorkflowDiagram.test.ts
@@ -19,7 +19,7 @@ describe('generateWorkflowDiagram', () => {
const result = generateWorkflowDiagram({
trigger,
steps,
- defaultEdgeType: 'empty-filter--editable',
+ workflowContext: 'workflow',
});
expect(result.nodes).toHaveLength(1);
@@ -86,7 +86,7 @@ describe('generateWorkflowDiagram', () => {
const result = generateWorkflowDiagram({
trigger,
steps,
- defaultEdgeType: 'empty-filter--editable',
+ workflowContext: 'workflow',
});
expect(result.nodes).toHaveLength(steps.length + 1); // All steps + trigger
@@ -165,7 +165,7 @@ describe('generateWorkflowDiagram', () => {
const result = generateWorkflowDiagram({
trigger,
steps,
- defaultEdgeType: 'empty-filter--editable',
+ workflowContext: 'workflow',
});
expect(result.edges.length).toEqual(2);
@@ -232,7 +232,7 @@ describe('generateWorkflowDiagram', () => {
const result = generateWorkflowDiagram({
trigger,
steps,
- defaultEdgeType: 'empty-filter--editable',
+ workflowContext: 'workflow',
});
expect(result.edges.length).toEqual(2);
@@ -318,7 +318,7 @@ describe('generateWorkflowDiagram', () => {
const result = generateWorkflowDiagram({
trigger,
steps,
- defaultEdgeType: 'empty-filter--editable',
+ workflowContext: 'workflow',
});
expect(result.edges.length).toEqual(4);
diff --git a/packages/twenty-front/src/modules/workflow/workflow-diagram/utils/__tests__/getWorkflowVersionDiagram.test.ts b/packages/twenty-front/src/modules/workflow/workflow-diagram/utils/__tests__/getWorkflowVersionDiagram.test.ts
index 00377b368b..f590001f25 100644
--- a/packages/twenty-front/src/modules/workflow/workflow-diagram/utils/__tests__/getWorkflowVersionDiagram.test.ts
+++ b/packages/twenty-front/src/modules/workflow/workflow-diagram/utils/__tests__/getWorkflowVersionDiagram.test.ts
@@ -9,7 +9,7 @@ describe('getWorkflowVersionDiagram', () => {
it('returns an empty diagram if the provided workflow version', () => {
const result = getWorkflowVersionDiagram({
workflowVersion: undefined,
- isEditable: true,
+ workflowContext: 'workflow',
});
expect(result).toMatchInlineSnapshot(`
@@ -33,7 +33,7 @@ describe('getWorkflowVersionDiagram', () => {
updatedAt: '',
workflowId: '',
},
- isEditable: true,
+ workflowContext: 'workflow',
});
expect(result).toMatchInlineSnapshot(`
@@ -77,7 +77,7 @@ describe('getWorkflowVersionDiagram', () => {
updatedAt: '',
workflowId: '',
},
- isEditable: true,
+ workflowContext: 'workflow',
});
expect(result).toMatchInlineSnapshot(`
@@ -145,7 +145,7 @@ describe('getWorkflowVersionDiagram', () => {
updatedAt: '',
workflowId: '',
},
- isEditable: true,
+ workflowContext: 'workflow',
});
expect(result).toMatchInlineSnapshot(`
diff --git a/packages/twenty-front/src/modules/workflow/workflow-diagram/utils/__tests__/transformFilterNodesAsEdges.test.ts b/packages/twenty-front/src/modules/workflow/workflow-diagram/utils/__tests__/transformFilterNodesAsEdges.test.ts
index 1c4fcd2cfc..d31a4b12f1 100644
--- a/packages/twenty-front/src/modules/workflow/workflow-diagram/utils/__tests__/transformFilterNodesAsEdges.test.ts
+++ b/packages/twenty-front/src/modules/workflow/workflow-diagram/utils/__tests__/transformFilterNodesAsEdges.test.ts
@@ -45,7 +45,7 @@ describe('transformFilterNodesAsEdges', () => {
const result = transformFilterNodesAsEdges({
nodes: diagram.nodes,
edges: diagram.edges,
- defaultFilterEdgeType: 'filter--editable',
+ workflowContext: 'workflow',
});
expect(result.nodes).toEqual(diagram.nodes);
@@ -111,7 +111,7 @@ describe('transformFilterNodesAsEdges', () => {
const result = transformFilterNodesAsEdges({
nodes: diagram.nodes,
edges: diagram.edges,
- defaultFilterEdgeType: 'filter--editable',
+ workflowContext: 'workflow',
});
// Should only have nodes A and C
@@ -256,7 +256,7 @@ describe('transformFilterNodesAsEdges', () => {
const result = transformFilterNodesAsEdges({
nodes: diagram.nodes,
edges: diagram.edges,
- defaultFilterEdgeType: 'filter--editable',
+ workflowContext: 'workflow',
});
// Should only have nodes A, C, and D
@@ -348,7 +348,7 @@ describe('transformFilterNodesAsEdges', () => {
const result = transformFilterNodesAsEdges({
nodes: diagram.nodes,
edges: diagram.edges,
- defaultFilterEdgeType: 'filter--editable',
+ workflowContext: 'workflow',
});
// Should only have node A (filter node B is removed)
@@ -430,7 +430,7 @@ describe('transformFilterNodesAsEdges', () => {
const result = transformFilterNodesAsEdges({
nodes: diagram.nodes,
edges: diagram.edges,
- defaultFilterEdgeType: 'filter--editable',
+ workflowContext: 'workflow',
});
// Should have trigger and C nodes
diff --git a/packages/twenty-front/src/modules/workflow/workflow-diagram/utils/generateNodesAndEdgesForDefaultNode.ts b/packages/twenty-front/src/modules/workflow/workflow-diagram/utils/generateNodesAndEdgesForDefaultNode.ts
new file mode 100644
index 0000000000..1febeee5c2
--- /dev/null
+++ b/packages/twenty-front/src/modules/workflow/workflow-diagram/utils/generateNodesAndEdgesForDefaultNode.ts
@@ -0,0 +1,75 @@
+import { 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 { getEdgeTypeBetweenTwoNodes } from '@/workflow/workflow-diagram/utils/getEdgeTypeBetweenTwoNodes';
+import { WORKFLOW_VISUALIZER_EDGE_DEFAULT_CONFIGURATION } from '@/workflow/workflow-diagram/workflow-edges/constants/WorkflowVisualizerEdgeDefaultConfiguration';
+import { isDefined } from 'twenty-shared/utils';
+import { v4 } from 'uuid';
+
+export const generateNodesAndEdgesForDefaultNode = ({
+ step,
+ xPos,
+ yPos,
+ nodes,
+ edges,
+ workflowContext,
+}: {
+ step: WorkflowStep;
+ yPos: number;
+ xPos: number;
+ nodes: readonly WorkflowDiagramNode[];
+ edges: readonly WorkflowDiagramEdge[];
+ workflowContext: WorkflowContext;
+}): {
+ nodes: Array;
+ edges: Array;
+} => {
+ const edgeTypeBetweenTwoNodes = getEdgeTypeBetweenTwoNodes({
+ workflowContext,
+ });
+
+ const updatedNodes = [...nodes];
+ const updatedEdges = [...edges];
+
+ updatedNodes.push({
+ id: step.id,
+ data: {
+ nodeType: 'action',
+ actionType: step.type,
+ name: step.name,
+ hasNextStepIds:
+ isDefined(step.nextStepIds) && step.nextStepIds.length > 0,
+ stepId: step.id,
+ position: step.position ?? {
+ x: xPos,
+ y: yPos,
+ },
+ } satisfies WorkflowDiagramStepNodeData,
+ position: step.position ?? {
+ x: xPos,
+ y: yPos,
+ },
+ });
+
+ step.nextStepIds?.forEach((child) => {
+ updatedEdges.push({
+ ...WORKFLOW_VISUALIZER_EDGE_DEFAULT_CONFIGURATION,
+ type: edgeTypeBetweenTwoNodes,
+ id: v4(),
+ source: step.id,
+ target: child,
+ ...(edgeTypeBetweenTwoNodes.includes('editable')
+ ? { deletable: true, selectable: true }
+ : {}),
+ });
+ });
+
+ return {
+ nodes: updatedNodes,
+ edges: updatedEdges,
+ };
+};
diff --git a/packages/twenty-front/src/modules/workflow/workflow-diagram/utils/generateNodesAndEdgesForIteratorNode.ts b/packages/twenty-front/src/modules/workflow/workflow-diagram/utils/generateNodesAndEdgesForIteratorNode.ts
new file mode 100644
index 0000000000..1445b59e5a
--- /dev/null
+++ b/packages/twenty-front/src/modules/workflow/workflow-diagram/utils/generateNodesAndEdgesForIteratorNode.ts
@@ -0,0 +1,176 @@
+import { 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 { getEdgeTypeBetweenTwoNodes } from '@/workflow/workflow-diagram/utils/getEdgeTypeBetweenTwoNodes';
+import { WORKFLOW_VISUALIZER_EDGE_DEFAULT_CONFIGURATION } from '@/workflow/workflow-diagram/workflow-edges/constants/WorkflowVisualizerEdgeDefaultConfiguration';
+import { type WorkflowDiagramIteratorEmptyActionNodeData } from '@/workflow/workflow-diagram/workflow-iterator/types/WorkflowDiagramIteratorEmptyActionNodeData';
+import { WORKFLOW_DIAGRAM_ITERATOR_NODE_LOOP_HANDLE_ID } from '@/workflow/workflow-diagram/workflow-nodes/constants/WorkflowDiagramIteratorNodeLoopHandleId';
+import { msg } from '@lingui/core/macro';
+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,
+ xPos,
+ yPos,
+ nodes,
+ edges,
+ workflowContext,
+}: {
+ step: WorkflowStep & { type: 'ITERATOR' };
+ yPos: number;
+ xPos: number;
+ nodes: readonly WorkflowDiagramNode[];
+ edges: readonly WorkflowDiagramEdge[];
+ workflowContext: WorkflowContext;
+}): {
+ nodes: Array;
+ edges: Array;
+} => {
+ const edgeTypeBetweenTwoNodes = getEdgeTypeBetweenTwoNodes({
+ workflowContext,
+ });
+
+ const updatedNodes = [...nodes];
+ const updatedEdges = [...edges];
+
+ const iteratorNode: WorkflowDiagramNode = {
+ id: step.id,
+ data: {
+ nodeType: 'action',
+ actionType: step.type,
+ name: step.name,
+ hasNextStepIds:
+ isDefined(step.nextStepIds) && step.nextStepIds.length > 0,
+ stepId: step.id,
+ position: step.position ?? {
+ x: xPos,
+ y: yPos,
+ },
+ defaultHandleOptions: {
+ label: msg`completed`,
+ },
+ rightHandleOptions: {
+ id: WORKFLOW_DIAGRAM_ITERATOR_NODE_LOOP_HANDLE_ID,
+ },
+ } satisfies WorkflowDiagramStepNodeData,
+ position: step.position ?? {
+ x: xPos,
+ y: yPos,
+ },
+ };
+
+ updatedNodes.push(iteratorNode);
+
+ const initialLoopStepIds = Array.isArray(
+ step.settings.input.initialLoopStepIds,
+ )
+ ? step.settings.input.initialLoopStepIds
+ : isNonEmptyString(step.settings.input.initialLoopStepIds)
+ ? [step.settings.input.initialLoopStepIds]
+ : [];
+
+ if (isNonEmptyArray(initialLoopStepIds)) {
+ for (const initialLoopStepId of initialLoopStepIds) {
+ updatedEdges.push({
+ ...WORKFLOW_VISUALIZER_EDGE_DEFAULT_CONFIGURATION,
+ type: edgeTypeBetweenTwoNodes,
+ id: v4(),
+ source: step.id,
+ sourceHandle: WORKFLOW_DIAGRAM_ITERATOR_NODE_LOOP_HANDLE_ID,
+ target: initialLoopStepId,
+ ...(edgeTypeBetweenTwoNodes.includes('editable')
+ ? { deletable: true, selectable: true }
+ : {}),
+ data: {
+ ...WORKFLOW_VISUALIZER_EDGE_DEFAULT_CONFIGURATION.data,
+ labelOptions: {
+ position: Position.Right,
+ label: msg`loop`,
+ },
+ },
+ });
+ }
+ } else {
+ const emptyNodeId = `${step.id}-empty-loop`;
+
+ updatedNodes.push({
+ id: emptyNodeId,
+ type: 'iterator-empty-action',
+ data: {
+ nodeType: 'iterator-empty-action',
+ parentIteratorStepId: step.id,
+ position: {
+ x: iteratorNode.position.x + 175,
+ y: iteratorNode.position.y + 75,
+ },
+ } satisfies WorkflowDiagramIteratorEmptyActionNodeData,
+ position: {
+ x: iteratorNode.position.x + 175,
+ y: iteratorNode.position.y + 75,
+ },
+ draggable: false,
+ });
+
+ updatedEdges.push({
+ ...WORKFLOW_VISUALIZER_EDGE_DEFAULT_CONFIGURATION,
+ type: edgeTypeBetweenTwoNodes,
+ id: v4(),
+ source: step.id,
+ sourceHandle: WORKFLOW_DIAGRAM_ITERATOR_NODE_LOOP_HANDLE_ID,
+ target: emptyNodeId,
+ ...(edgeTypeBetweenTwoNodes.includes('editable')
+ ? { deletable: true, selectable: true }
+ : {}),
+ data: {
+ ...WORKFLOW_VISUALIZER_EDGE_DEFAULT_CONFIGURATION.data,
+ labelOptions: {
+ position: Position.Right,
+ label: msg`loop`,
+ },
+ },
+ });
+
+ updatedEdges.push({
+ ...WORKFLOW_VISUALIZER_EDGE_DEFAULT_CONFIGURATION,
+ type: edgeTypeBetweenTwoNodes,
+ id: v4(),
+ source: emptyNodeId,
+ target: step.id,
+ ...(edgeTypeBetweenTwoNodes.includes('editable')
+ ? { deletable: true, selectable: true }
+ : {}),
+ });
+ }
+
+ step.nextStepIds?.forEach((child) => {
+ updatedEdges.push({
+ ...WORKFLOW_VISUALIZER_EDGE_DEFAULT_CONFIGURATION,
+ type: edgeTypeBetweenTwoNodes,
+ id: v4(),
+ source: step.id,
+ target: child,
+ ...(edgeTypeBetweenTwoNodes.includes('editable')
+ ? { deletable: true, selectable: true }
+ : {}),
+ data: {
+ ...WORKFLOW_VISUALIZER_EDGE_DEFAULT_CONFIGURATION.data,
+ labelOptions: {
+ position: Position.Bottom,
+ label: msg`completed`,
+ },
+ },
+ });
+ });
+
+ return {
+ nodes: updatedNodes,
+ edges: updatedEdges,
+ };
+};
diff --git a/packages/twenty-front/src/modules/workflow/workflow-diagram/utils/generateWorkflowDiagram.ts b/packages/twenty-front/src/modules/workflow/workflow-diagram/utils/generateWorkflowDiagram.ts
index b29c9cb0ac..d6154ea890 100644
--- a/packages/twenty-front/src/modules/workflow/workflow-diagram/utils/generateWorkflowDiagram.ts
+++ b/packages/twenty-front/src/modules/workflow/workflow-diagram/utils/generateWorkflowDiagram.ts
@@ -4,17 +4,18 @@ import {
} from '@/workflow/types/Workflow';
import { FIRST_NODE_POSITION } from '@/workflow/workflow-diagram/constants/FirstNodePosition';
import { VERTICAL_DISTANCE_BETWEEN_TWO_NODES } from '@/workflow/workflow-diagram/constants/VerticalDistanceBetweenTwoNodes';
+import { WORKFLOW_DIAGRAM_EMPTY_TRIGGER_NODE_DEFINITION } from '@/workflow/workflow-diagram/constants/WorkflowDiagramEmptyTriggerNodeDefinition';
+import { type WorkflowContext } from '@/workflow/workflow-diagram/types/WorkflowContext';
import {
type WorkflowDiagram,
type WorkflowDiagramEdge,
- type WorkflowDiagramEdgeType,
type WorkflowDiagramNode,
- type WorkflowDiagramStepNodeData,
} from '@/workflow/workflow-diagram/types/WorkflowDiagram';
+import { generateNodesAndEdgesForDefaultNode } from '@/workflow/workflow-diagram/utils/generateNodesAndEdgesForDefaultNode';
+import { generateNodesAndEdgesForIteratorNode } from '@/workflow/workflow-diagram/utils/generateNodesAndEdgesForIteratorNode';
+import { getEdgeTypeBetweenTwoNodes } from '@/workflow/workflow-diagram/utils/getEdgeTypeBetweenTwoNodes';
import { getWorkflowDiagramTriggerNode } from '@/workflow/workflow-diagram/utils/getWorkflowDiagramTriggerNode';
import { WORKFLOW_VISUALIZER_EDGE_DEFAULT_CONFIGURATION } from '@/workflow/workflow-diagram/workflow-edges/constants/WorkflowVisualizerEdgeDefaultConfiguration';
-
-import { WORKFLOW_DIAGRAM_EMPTY_TRIGGER_NODE_DEFINITION } from '@/workflow/workflow-diagram/constants/WorkflowDiagramEmptyTriggerNodeDefinition';
import { isDefined } from 'twenty-shared/utils';
import { TRIGGER_STEP_ID } from 'twenty-shared/workflow';
import { v4 } from 'uuid';
@@ -22,14 +23,18 @@ import { v4 } from 'uuid';
export const generateWorkflowDiagram = ({
trigger,
steps,
- defaultEdgeType,
+ workflowContext,
}: {
trigger: WorkflowTrigger | undefined;
steps: Array;
- defaultEdgeType: WorkflowDiagramEdgeType;
+ workflowContext: WorkflowContext;
}): WorkflowDiagram => {
- const nodes: Array = [];
- const edges: Array = [];
+ let nodes: Array = [];
+ let edges: Array = [];
+
+ const edgeTypeBetweenTwoNodes = getEdgeTypeBetweenTwoNodes({
+ workflowContext,
+ });
if (isDefined(trigger)) {
nodes.push(getWorkflowDiagramTriggerNode({ trigger }));
@@ -37,60 +42,59 @@ export const generateWorkflowDiagram = ({
nodes.push(WORKFLOW_DIAGRAM_EMPTY_TRIGGER_NODE_DEFINITION);
}
- let levelYPos = FIRST_NODE_POSITION.y;
-
- const xPos = FIRST_NODE_POSITION.x;
-
- for (const step of steps) {
- levelYPos += VERTICAL_DISTANCE_BETWEEN_TWO_NODES;
-
- nodes.push({
- id: step.id,
- data: {
- nodeType: 'action',
- actionType: step.type,
- name: step.name,
- hasNextStepIds:
- isDefined(step.nextStepIds) && step.nextStepIds.length > 0,
- stepId: step.id,
- position: step.position ?? {
- x: xPos,
- y: levelYPos,
- },
- } satisfies WorkflowDiagramStepNodeData,
- position: step.position ?? {
- x: xPos,
- y: levelYPos,
- },
- });
- }
-
for (const stepLinkToTriggerId of trigger?.nextStepIds ?? []) {
edges.push({
...WORKFLOW_VISUALIZER_EDGE_DEFAULT_CONFIGURATION,
- type: defaultEdgeType,
+ type: edgeTypeBetweenTwoNodes,
id: v4(),
source: TRIGGER_STEP_ID,
target: stepLinkToTriggerId,
- ...(defaultEdgeType.includes('editable')
+ ...(edgeTypeBetweenTwoNodes.includes('editable')
? { deletable: true, selectable: true }
: {}),
});
}
+ const xPos = FIRST_NODE_POSITION.x;
+ let levelYPos = FIRST_NODE_POSITION.y;
+
for (const step of steps) {
- step.nextStepIds?.forEach((child) => {
- edges.push({
- ...WORKFLOW_VISUALIZER_EDGE_DEFAULT_CONFIGURATION,
- type: defaultEdgeType,
- id: v4(),
- source: step.id,
- target: child,
- ...(defaultEdgeType.includes('editable')
- ? { deletable: true, selectable: true }
- : {}),
- });
- });
+ levelYPos += VERTICAL_DISTANCE_BETWEEN_TWO_NODES;
+
+ switch (step.type) {
+ case 'ITERATOR': {
+ const { nodes: iteratorNodes, edges: iteratorEdges } =
+ generateNodesAndEdgesForIteratorNode({
+ step,
+ xPos,
+ yPos: levelYPos,
+ nodes,
+ edges,
+ workflowContext,
+ });
+
+ nodes = iteratorNodes;
+ edges = iteratorEdges;
+
+ break;
+ }
+ default: {
+ const { nodes: defaultNodes, edges: defaultEdges } =
+ generateNodesAndEdgesForDefaultNode({
+ step,
+ xPos,
+ yPos: levelYPos,
+ nodes,
+ edges,
+ workflowContext,
+ });
+
+ nodes = defaultNodes;
+ edges = defaultEdges;
+
+ break;
+ }
+ }
}
return {
diff --git a/packages/twenty-front/src/modules/workflow/workflow-diagram/utils/generateWorkflowRunDiagram.ts b/packages/twenty-front/src/modules/workflow/workflow-diagram/utils/generateWorkflowRunDiagram.ts
index 059125ff3a..a14e47d1a2 100644
--- a/packages/twenty-front/src/modules/workflow/workflow-diagram/utils/generateWorkflowRunDiagram.ts
+++ b/packages/twenty-front/src/modules/workflow/workflow-diagram/utils/generateWorkflowRunDiagram.ts
@@ -42,7 +42,7 @@ export const generateWorkflowRunDiagram = ({
const workflowDiagram = generateWorkflowDiagram({
trigger,
steps,
- defaultEdgeType: 'empty-filter--readonly',
+ workflowContext: 'workflow-run',
});
const workflowRunDiagramNodes: WorkflowRunDiagramNode[] =
@@ -104,7 +104,7 @@ export const generateWorkflowRunDiagram = ({
diagram: transformFilterNodesAsEdges({
nodes: workflowRunDiagramNodes,
edges: workflowRunDiagramEdges,
- defaultFilterEdgeType: 'filter--run',
+ workflowContext: 'workflow-run',
}),
stepToOpenByDefault,
};
diff --git a/packages/twenty-front/src/modules/workflow/workflow-diagram/utils/getEdgeTypeBetweenTwoNodes.ts b/packages/twenty-front/src/modules/workflow/workflow-diagram/utils/getEdgeTypeBetweenTwoNodes.ts
new file mode 100644
index 0000000000..f488162a4e
--- /dev/null
+++ b/packages/twenty-front/src/modules/workflow/workflow-diagram/utils/getEdgeTypeBetweenTwoNodes.ts
@@ -0,0 +1,18 @@
+import { type WorkflowContext } from '@/workflow/workflow-diagram/types/WorkflowContext';
+import { type WorkflowDiagramEdgeType } from '@/workflow/workflow-diagram/types/WorkflowDiagram';
+
+export const getEdgeTypeBetweenTwoNodes = ({
+ workflowContext,
+}: {
+ workflowContext: WorkflowContext;
+}): WorkflowDiagramEdgeType => {
+ switch (workflowContext) {
+ case 'workflow': {
+ return 'empty-filter--editable';
+ }
+ case 'workflow-version':
+ case 'workflow-run': {
+ return 'empty-filter--readonly';
+ }
+ }
+};
diff --git a/packages/twenty-front/src/modules/workflow/workflow-diagram/utils/getFilterEdgeType.ts b/packages/twenty-front/src/modules/workflow/workflow-diagram/utils/getFilterEdgeType.ts
new file mode 100644
index 0000000000..5502336cc8
--- /dev/null
+++ b/packages/twenty-front/src/modules/workflow/workflow-diagram/utils/getFilterEdgeType.ts
@@ -0,0 +1,20 @@
+import { type WorkflowContext } from '@/workflow/workflow-diagram/types/WorkflowContext';
+import { type WorkflowDiagramEdgeType } from '@/workflow/workflow-diagram/types/WorkflowDiagram';
+
+export const getFilterEdgeType = ({
+ workflowContext,
+}: {
+ workflowContext: WorkflowContext;
+}): WorkflowDiagramEdgeType => {
+ switch (workflowContext) {
+ case 'workflow': {
+ return 'filter--editable';
+ }
+ case 'workflow-version': {
+ return 'filter--readonly';
+ }
+ case 'workflow-run': {
+ return 'filter--run';
+ }
+ }
+};
diff --git a/packages/twenty-front/src/modules/workflow/workflow-diagram/utils/getWorkflowVersionDiagram.ts b/packages/twenty-front/src/modules/workflow/workflow-diagram/utils/getWorkflowVersionDiagram.ts
index 281077299f..b4bded0736 100644
--- a/packages/twenty-front/src/modules/workflow/workflow-diagram/utils/getWorkflowVersionDiagram.ts
+++ b/packages/twenty-front/src/modules/workflow/workflow-diagram/utils/getWorkflowVersionDiagram.ts
@@ -1,8 +1,6 @@
import { type WorkflowVersion } from '@/workflow/types/Workflow';
-import {
- type WorkflowDiagram,
- type WorkflowDiagramEdgeType,
-} from '@/workflow/workflow-diagram/types/WorkflowDiagram';
+import { type WorkflowContext } from '@/workflow/workflow-diagram/types/WorkflowContext';
+import { type WorkflowDiagram } from '@/workflow/workflow-diagram/types/WorkflowDiagram';
import { generateWorkflowDiagram } from '@/workflow/workflow-diagram/utils/generateWorkflowDiagram';
import { transformFilterNodesAsEdges } from '@/workflow/workflow-diagram/utils/transformFilterNodesAsEdges';
import { isDefined } from 'twenty-shared/utils';
@@ -12,20 +10,12 @@ const EMPTY_DIAGRAM: WorkflowDiagram = {
edges: [],
};
-const getEdgeTypeToCreateByDefault = ({
- isEditable,
-}: {
- isEditable: boolean;
-}): WorkflowDiagramEdgeType => {
- return isEditable ? 'empty-filter--editable' : 'empty-filter--readonly';
-};
-
export const getWorkflowVersionDiagram = ({
workflowVersion,
- isEditable,
+ workflowContext,
}: {
workflowVersion: WorkflowVersion | undefined;
- isEditable: boolean;
+ workflowContext: WorkflowContext;
}): WorkflowDiagram => {
if (!isDefined(workflowVersion)) {
return EMPTY_DIAGRAM;
@@ -34,14 +24,12 @@ export const getWorkflowVersionDiagram = ({
const diagram = generateWorkflowDiagram({
trigger: workflowVersion.trigger ?? undefined,
steps: workflowVersion.steps ?? [],
- defaultEdgeType: getEdgeTypeToCreateByDefault({
- isEditable,
- }),
+ workflowContext,
});
return transformFilterNodesAsEdges({
nodes: diagram.nodes,
edges: diagram.edges,
- defaultFilterEdgeType: isEditable ? 'filter--editable' : 'filter--readonly',
+ workflowContext,
});
};
diff --git a/packages/twenty-front/src/modules/workflow/workflow-diagram/utils/transformFilterNodesAsEdges.ts b/packages/twenty-front/src/modules/workflow/workflow-diagram/utils/transformFilterNodesAsEdges.ts
index 351c8ebded..da43315698 100644
--- a/packages/twenty-front/src/modules/workflow/workflow-diagram/utils/transformFilterNodesAsEdges.ts
+++ b/packages/twenty-front/src/modules/workflow/workflow-diagram/utils/transformFilterNodesAsEdges.ts
@@ -1,8 +1,9 @@
+import { type WorkflowContext } from '@/workflow/workflow-diagram/types/WorkflowContext';
import {
type WorkflowDiagramEdge,
- type WorkflowDiagramEdgeType,
type WorkflowDiagramNode,
} from '@/workflow/workflow-diagram/types/WorkflowDiagram';
+import { getFilterEdgeType } from '@/workflow/workflow-diagram/utils/getFilterEdgeType';
import { isDefined } from 'twenty-shared/utils';
export const transformFilterNodesAsEdges = <
@@ -11,11 +12,11 @@ export const transformFilterNodesAsEdges = <
>({
nodes,
edges,
- defaultFilterEdgeType,
+ workflowContext,
}: {
nodes: T[];
edges: U[];
- defaultFilterEdgeType: WorkflowDiagramEdgeType;
+ workflowContext: WorkflowContext;
}): { nodes: T[]; edges: U[] } => {
const filterNodes = nodes.filter(
(node) =>
@@ -48,9 +49,11 @@ export const transformFilterNodesAsEdges = <
throw new Error('Expected the filter node to be of action type');
}
+ const filterEdgeType = getFilterEdgeType({ workflowContext });
+
const newEdge: U = {
...incomingEdge,
- type: defaultFilterEdgeType,
+ type: filterEdgeType,
id: `${incomingEdge.source}-${outgoingEdge.target}-filter-${filterNode.id}`,
target: outgoingEdge.target,
selectable: true,
diff --git a/packages/twenty-front/src/modules/workflow/workflow-diagram/workflow-edges/components/WorkflowDiagramDefaultEdgeEditable.tsx b/packages/twenty-front/src/modules/workflow/workflow-diagram/workflow-edges/components/WorkflowDiagramDefaultEdgeEditable.tsx
index 8f3296d33b..b2d7bfa2af 100644
--- a/packages/twenty-front/src/modules/workflow/workflow-diagram/workflow-edges/components/WorkflowDiagramDefaultEdgeEditable.tsx
+++ b/packages/twenty-front/src/modules/workflow/workflow-diagram/workflow-edges/components/WorkflowDiagramDefaultEdgeEditable.tsx
@@ -8,12 +8,15 @@ import { useStartNodeCreation } from '@/workflow/workflow-diagram/hooks/useStart
import { type WorkflowDiagramEdge } from '@/workflow/workflow-diagram/types/WorkflowDiagram';
import { WorkflowDiagramBaseEdge } from '@/workflow/workflow-diagram/workflow-edges/components/WorkflowDiagramBaseEdge';
import { WorkflowDiagramEdgeButtonGroup } from '@/workflow/workflow-diagram/workflow-edges/components/WorkflowDiagramEdgeButtonGroup';
+import { WorkflowDiagramEdgeLabel } from '@/workflow/workflow-diagram/workflow-edges/components/WorkflowDiagramEdgeLabel';
+import { WorkflowDiagramEdgeLabelContainer } from '@/workflow/workflow-diagram/workflow-edges/components/WorkflowDiagramEdgeLabelContainer';
import { WorkflowDiagramEdgeV2Container } from '@/workflow/workflow-diagram/workflow-edges/components/WorkflowDiagramEdgeV2Container';
import { WorkflowDiagramEdgeV2VisibilityContainer } from '@/workflow/workflow-diagram/workflow-edges/components/WorkflowDiagramEdgeV2VisibilityContainer';
import { WORKFLOW_DIAGRAM_EDGE_OPTIONS_CLICK_OUTSIDE_ID } from '@/workflow/workflow-diagram/workflow-edges/constants/WorkflowDiagramEdgeOptionsClickOutsideId';
import { useEdgeState } from '@/workflow/workflow-diagram/workflow-edges/hooks/useEdgeState';
import { useCreateStep } from '@/workflow/workflow-steps/hooks/useCreateStep';
import { useDeleteEdge } from '@/workflow/workflow-steps/hooks/useDeleteEdge';
+import { useLingui } from '@lingui/react/macro';
import {
EdgeLabelRenderer,
type EdgeProps,
@@ -35,7 +38,10 @@ export const WorkflowDiagramDefaultEdgeEditable = ({
targetY,
markerStart,
markerEnd,
+ data,
}: WorkflowDiagramDefaultEdgeEditableProps) => {
+ const { i18n } = useLingui();
+
const { isInRightDrawer } = useContext(ActionMenuContext);
const { isEdgeHovered } = useEdgeState();
@@ -116,6 +122,16 @@ export const WorkflowDiagramDefaultEdgeEditable = ({
/>
+ {isDefined(data?.labelOptions) && (
+
+
+
+ )}
+
theme.background.secondary};
+ border: 1px solid ${({ theme }) => theme.border.color.strong};
+ border-radius: ${({ theme }) => theme.border.radius.sm};
+ box-sizing: border-box;
+ display: flex;
+ gap: ${({ theme }) => theme.spacing(1)};
+ justify-content: center;
+ padding-block: ${({ theme }) => theme.spacing(0.5)};
+ padding-inline: ${({ theme }) => theme.spacing(1)};
+`;
+
+export const WorkflowDiagramEdgeLabel = ({ label }: { label: string }) => {
+ return (
+
+
+
+ );
+};
diff --git a/packages/twenty-front/src/modules/workflow/workflow-diagram/workflow-edges/components/WorkflowDiagramEdgeLabelContainer.tsx b/packages/twenty-front/src/modules/workflow/workflow-diagram/workflow-edges/components/WorkflowDiagramEdgeLabelContainer.tsx
new file mode 100644
index 0000000000..80129c99ed
--- /dev/null
+++ b/packages/twenty-front/src/modules/workflow/workflow-diagram/workflow-edges/components/WorkflowDiagramEdgeLabelContainer.tsx
@@ -0,0 +1,31 @@
+import { css } from '@emotion/react';
+import styled from '@emotion/styled';
+import { Position } from '@xyflow/react';
+
+const StyledWorkflowDiagramEdgeLabelContainer = styled.div<{
+ sourceX: number;
+ sourceY: number;
+ position: Position;
+}>`
+ position: absolute;
+ width: fit-content;
+
+ ${({ position, sourceX, sourceY }) => {
+ switch (position) {
+ case Position.Right: {
+ return css`
+ transform: translate(0%, -50%)
+ translate(${Math.floor(sourceX)}px, ${sourceY}px) translateX(10px);
+ `;
+ }
+ case Position.Bottom: {
+ return css`
+ transform: translate(-50%, 0%)
+ translate(${sourceX}px, ${Math.floor(sourceY)}px) translateY(10px);
+ `;
+ }
+ }
+ }}
+`;
+
+export { StyledWorkflowDiagramEdgeLabelContainer as WorkflowDiagramEdgeLabelContainer };
diff --git a/packages/twenty-front/src/modules/workflow/workflow-diagram/workflow-edges/components/WorkflowDiagramFilterEdgeEditable.tsx b/packages/twenty-front/src/modules/workflow/workflow-diagram/workflow-edges/components/WorkflowDiagramFilterEdgeEditable.tsx
index bd09d36322..d4a754d4cd 100644
--- a/packages/twenty-front/src/modules/workflow/workflow-diagram/workflow-edges/components/WorkflowDiagramFilterEdgeEditable.tsx
+++ b/packages/twenty-front/src/modules/workflow/workflow-diagram/workflow-edges/components/WorkflowDiagramFilterEdgeEditable.tsx
@@ -20,6 +20,8 @@ import {
import { getWorkflowDiagramColors } from '@/workflow/workflow-diagram/utils/getWorkflowDiagramColors';
import { WorkflowDiagramBaseEdge } from '@/workflow/workflow-diagram/workflow-edges/components/WorkflowDiagramBaseEdge';
import { WorkflowDiagramEdgeButtonGroup } from '@/workflow/workflow-diagram/workflow-edges/components/WorkflowDiagramEdgeButtonGroup';
+import { WorkflowDiagramEdgeLabel } from '@/workflow/workflow-diagram/workflow-edges/components/WorkflowDiagramEdgeLabel';
+import { WorkflowDiagramEdgeLabelContainer } from '@/workflow/workflow-diagram/workflow-edges/components/WorkflowDiagramEdgeLabelContainer';
import { WorkflowDiagramEdgeV2Container } from '@/workflow/workflow-diagram/workflow-edges/components/WorkflowDiagramEdgeV2Container';
import { WorkflowDiagramEdgeV2VisibilityContainer } from '@/workflow/workflow-diagram/workflow-edges/components/WorkflowDiagramEdgeV2VisibilityContainer';
import { WORKFLOW_DIAGRAM_EDGE_OPTIONS_CLICK_OUTSIDE_ID } from '@/workflow/workflow-diagram/workflow-edges/constants/WorkflowDiagramEdgeOptionsClickOutsideId';
@@ -39,6 +41,7 @@ import {
} from '@xyflow/react';
import { useContext } from 'react';
import { useSetRecoilState } from 'recoil';
+import { isDefined } from 'twenty-shared/utils';
import {
IconDotsVertical,
@@ -94,7 +97,8 @@ export const WorkflowDiagramFilterEdgeEditable = ({
}: WorkflowDiagramFilterEdgeEditableProps) => {
assertFilterEdgeDataOrThrow(data);
- const { t } = useLingui();
+ const { t, i18n } = useLingui();
+
const theme = useTheme();
const [edgePath, labelX, labelY] = getBezierPath({
@@ -192,6 +196,16 @@ export const WorkflowDiagramFilterEdgeEditable = ({
/>
+ {isDefined(data?.labelOptions) && (
+
+
+
+ )}
+
{
+ /**
+ * This function is used in a context where dependencies shouldn't change much.
+ * That's why its wrapped in a `useCallback` hook. Removing memoization might break the app unexpectedly.
+ */
+ const startIteratorFirstNodeCreation = useCallback(() => {
+ // TODO: to implement
+ }, []);
+
+ // TODO: to implement
+ const isIteratorFirstNodeCreationStarted = () => {
+ return false;
+ };
+
+ return {
+ startIteratorFirstNodeCreation,
+ isIteratorFirstNodeCreationStarted,
+ };
+};
diff --git a/packages/twenty-front/src/modules/workflow/workflow-diagram/workflow-iterator/types/WorkflowDiagramIteratorEmptyActionNodeData.ts b/packages/twenty-front/src/modules/workflow/workflow-diagram/workflow-iterator/types/WorkflowDiagramIteratorEmptyActionNodeData.ts
new file mode 100644
index 0000000000..4d880be21e
--- /dev/null
+++ b/packages/twenty-front/src/modules/workflow/workflow-diagram/workflow-iterator/types/WorkflowDiagramIteratorEmptyActionNodeData.ts
@@ -0,0 +1,8 @@
+export type WorkflowDiagramIteratorEmptyActionNodeData = {
+ nodeType: 'iterator-empty-action';
+ parentIteratorStepId: string;
+ position: {
+ x: number;
+ y: number;
+ };
+};
diff --git a/packages/twenty-front/src/modules/workflow/workflow-diagram/workflow-nodes/components/WorkflowDiagramHandleSource.tsx b/packages/twenty-front/src/modules/workflow/workflow-diagram/workflow-nodes/components/WorkflowDiagramHandleSource.tsx
index 75388392d1..2c0b756364 100644
--- a/packages/twenty-front/src/modules/workflow/workflow-diagram/workflow-nodes/components/WorkflowDiagramHandleSource.tsx
+++ b/packages/twenty-front/src/modules/workflow/workflow-diagram/workflow-nodes/components/WorkflowDiagramHandleSource.tsx
@@ -6,13 +6,6 @@ import { css } from '@emotion/react';
import styled from '@emotion/styled';
import { Handle, Position, type HandleProps } from '@xyflow/react';
-type WorkflowDiagramHandleSourceProps = {
- selected: boolean;
- hovered?: boolean;
- readOnly?: boolean;
- runStatus?: WorkflowRunStepStatus;
-};
-
const HANDLE_SCALE_ON_HOVER = 1.5;
const StyledHandle = styled(Handle, {
@@ -23,7 +16,7 @@ const StyledHandle = styled(Handle, {
prop !== 'runStatus',
})<{
type: HandleProps['type'];
- disableHoverEffect: boolean;
+ disableHoverEffect?: boolean;
selected: boolean;
hovered?: boolean;
runStatus?: WorkflowRunStepStatus;
@@ -37,7 +30,7 @@ const StyledHandle = styled(Handle, {
if (!selected) {
return css`
background: ${theme.background.primary};
- border-color: ${hovered && !disableHoverEffect
+ border-color: ${hovered && disableHoverEffect !== true
? theme.font.color.light
: theme.border.color.strong};
`;
@@ -56,45 +49,55 @@ const StyledHandle = styled(Handle, {
border-color 0.1s;
z-index: 1;
- transform: translate(-50%, 50%);
- transform-origin: bottom left;
+ ${({ position }) => {
+ if (position === Position.Right) {
+ return css`
+ transform: translate(50%, -50%);
+ transform-origin: top right;
+ `;
+ }
+
+ return css`
+ transform: translate(-50%, 50%);
+ transform-origin: bottom left;
+ `;
+ }}
&.connectionindicator {
cursor: pointer;
}
- ${({ disableHoverEffect, theme }) => {
- if (disableHoverEffect) {
- return undefined;
- }
+ &:hover {
+ ${({ disableHoverEffect, theme }) => {
+ if (disableHoverEffect === true) {
+ return undefined;
+ }
- const colors = getWorkflowDiagramColors({ theme });
+ const colors = getWorkflowDiagramColors({ theme });
- return css`
- &:hover {
+ return css`
background: ${colors.selected.background} !important;
border-color: ${colors.selected.borderColor} !important;
- transform: scale(${HANDLE_SCALE_ON_HOVER}) translate(-50%, 50%);
+ `;
+ }}
+
+ ${({ disableHoverEffect, position }) => {
+ if (disableHoverEffect === true) {
+ return undefined;
}
- `;
- }}
+
+ if (position === Position.Right) {
+ return css`
+ transform: scale(${HANDLE_SCALE_ON_HOVER}) translate(50%, -50%);
+ `;
+ }
+
+ return css`
+ transform: scale(${HANDLE_SCALE_ON_HOVER}) translate(-50%, 50%);
+ `;
+ }}
+ }
}
`;
-export const WorkflowDiagramHandleSource = ({
- selected,
- hovered = false,
- readOnly = false,
- runStatus,
-}: WorkflowDiagramHandleSourceProps) => {
- return (
-
- );
-};
+export { StyledHandle as WorkflowDiagramHandleSource };
diff --git a/packages/twenty-front/src/modules/workflow/workflow-diagram/workflow-nodes/components/WorkflowDiagramIteratorEmptyActionEditable.tsx b/packages/twenty-front/src/modules/workflow/workflow-diagram/workflow-nodes/components/WorkflowDiagramIteratorEmptyActionEditable.tsx
new file mode 100644
index 0000000000..c8cc7d3b3d
--- /dev/null
+++ b/packages/twenty-front/src/modules/workflow/workflow-diagram/workflow-nodes/components/WorkflowDiagramIteratorEmptyActionEditable.tsx
@@ -0,0 +1,82 @@
+import { WORKFLOW_DIAGRAM_STEP_NODE_BASE_CLICK_OUTSIDE_ID } from '@/workflow/workflow-diagram/constants/WorkflowDiagramStepNodeClickOutsideId';
+import { useEdgeState } from '@/workflow/workflow-diagram/workflow-edges/hooks/useEdgeState';
+import { useStartIteratorFirstNodeCreation } from '@/workflow/workflow-diagram/workflow-iterator/hooks/useStartIteratorFirstNodeCreation';
+import { type WorkflowDiagramIteratorEmptyActionNodeData } from '@/workflow/workflow-diagram/workflow-iterator/types/WorkflowDiagramIteratorEmptyActionNodeData';
+import { WorkflowDiagramHandleSource } from '@/workflow/workflow-diagram/workflow-nodes/components/WorkflowDiagramHandleSource';
+import { WorkflowDiagramHandleTarget } from '@/workflow/workflow-diagram/workflow-nodes/components/WorkflowDiagramHandleTarget';
+import { WorkflowNodeContainer } from '@/workflow/workflow-diagram/workflow-nodes/components/WorkflowNodeContainer';
+import { WorkflowNodeIconContainer } from '@/workflow/workflow-diagram/workflow-nodes/components/WorkflowNodeIconContainer';
+import { WorkflowNodeLabel } from '@/workflow/workflow-diagram/workflow-nodes/components/WorkflowNodeLabel';
+import { WorkflowNodeLabelWithCounterPart } from '@/workflow/workflow-diagram/workflow-nodes/components/WorkflowNodeLabelWithCounterPart';
+import { WorkflowNodeRightPart } from '@/workflow/workflow-diagram/workflow-nodes/components/WorkflowNodeRightPart';
+import { WorkflowNodeTitle } from '@/workflow/workflow-diagram/workflow-nodes/components/WorkflowNodeTitle';
+import { useConnectionState } from '@/workflow/workflow-diagram/workflow-nodes/hooks/useConnectionState';
+import { useLingui } from '@lingui/react/macro';
+import { Position } from '@xyflow/react';
+import { useState } from 'react';
+
+export const WorkflowDiagramIteratorEmptyActionEditable = ({
+ id,
+ selected,
+}: {
+ id: string;
+ selected: boolean;
+ data: WorkflowDiagramIteratorEmptyActionNodeData;
+}) => {
+ const { t } = useLingui();
+
+ const [isHovered, setIsHovered] = useState(false);
+
+ const handleMouseEnter = () => {
+ setIsHovered(true);
+ };
+
+ const handleMouseLeave = () => {
+ setIsHovered(false);
+ };
+
+ const { isConnectable, isSourceConnected } = useConnectionState('action');
+
+ const { isSourceSelected, isSourceHovered } = useEdgeState();
+
+ const { startIteratorFirstNodeCreation } =
+ useStartIteratorFirstNodeCreation();
+
+ const handleClick = () => {
+ startIteratorFirstNodeCreation();
+ };
+
+ return (
+
+
+
+
+
+
+
+ {t`Action`}
+
+
+ {t`Add an Action`}
+
+
+
+
+ );
+};
diff --git a/packages/twenty-front/src/modules/workflow/workflow-diagram/workflow-nodes/components/WorkflowDiagramStepNodeEditableContent.tsx b/packages/twenty-front/src/modules/workflow/workflow-diagram/workflow-nodes/components/WorkflowDiagramStepNodeEditableContent.tsx
index 62baaa5974..cf6bf063bb 100644
--- a/packages/twenty-front/src/modules/workflow/workflow-diagram/workflow-nodes/components/WorkflowDiagramStepNodeEditableContent.tsx
+++ b/packages/twenty-front/src/modules/workflow/workflow-diagram/workflow-nodes/components/WorkflowDiagramStepNodeEditableContent.tsx
@@ -2,6 +2,7 @@ import { WorkflowDiagramCreateStepElement } from '@/workflow/workflow-diagram/co
import { WORKFLOW_DIAGRAM_STEP_NODE_BASE_CLICK_OUTSIDE_ID } from '@/workflow/workflow-diagram/constants/WorkflowDiagramStepNodeClickOutsideId';
import { useStartNodeCreation } from '@/workflow/workflow-diagram/hooks/useStartNodeCreation';
import { type WorkflowDiagramStepNodeData } from '@/workflow/workflow-diagram/types/WorkflowDiagram';
+import { WorkflowDiagramEdgeLabel } from '@/workflow/workflow-diagram/workflow-edges/components/WorkflowDiagramEdgeLabel';
import { useEdgeState } from '@/workflow/workflow-diagram/workflow-edges/hooks/useEdgeState';
import { WorkflowDiagramHandleSource } from '@/workflow/workflow-diagram/workflow-nodes/components/WorkflowDiagramHandleSource';
import { WorkflowDiagramHandleTarget } from '@/workflow/workflow-diagram/workflow-nodes/components/WorkflowDiagramHandleTarget';
@@ -14,8 +15,10 @@ import { WorkflowNodeRightPart } from '@/workflow/workflow-diagram/workflow-node
import { WorkflowNodeTitle } from '@/workflow/workflow-diagram/workflow-nodes/components/WorkflowNodeTitle';
import { useConnectionState } from '@/workflow/workflow-diagram/workflow-nodes/hooks/useConnectionState';
import styled from '@emotion/styled';
+import { useLingui } from '@lingui/react/macro';
+import { Position } from '@xyflow/react';
import { useState } from 'react';
-import { capitalize } from 'twenty-shared/utils';
+import { capitalize, isDefined } from 'twenty-shared/utils';
import { IconTrash } from 'twenty-ui/display';
import { FloatingIconButton } from 'twenty-ui/input';
@@ -56,6 +59,8 @@ export const WorkflowDiagramStepNodeEditableContent = ({
onDelete: () => void;
onClick?: () => void;
}) => {
+ const { i18n } = useLingui();
+
const [isHovered, setIsHovered] = useState(false);
const handleMouseEnter = () => {
@@ -89,6 +94,7 @@ export const WorkflowDiagramStepNodeEditableContent = ({
isConnectable={isConnectable(id)}
>
+
@@ -123,11 +129,22 @@ export const WorkflowDiagramStepNodeEditableContent = ({
onMouseLeave={handleMouseLeave}
onClick={handleAddStepButtonContainerClick}
>
-
+
+ ) : undefined
+ }
+ />
)}
+
+ {isDefined(data.rightHandleOptions) && (
+
+ )}
>
);
};
diff --git a/packages/twenty-front/src/modules/workflow/workflow-diagram/workflow-nodes/components/WorkflowDiagramStepNodeReadonly.tsx b/packages/twenty-front/src/modules/workflow/workflow-diagram/workflow-nodes/components/WorkflowDiagramStepNodeReadonly.tsx
index 89e7a5d5b9..9d8e59b2de 100644
--- a/packages/twenty-front/src/modules/workflow/workflow-diagram/workflow-nodes/components/WorkflowDiagramStepNodeReadonly.tsx
+++ b/packages/twenty-front/src/modules/workflow/workflow-diagram/workflow-nodes/components/WorkflowDiagramStepNodeReadonly.tsx
@@ -9,6 +9,8 @@ import { WORKFLOW_DIAGRAM_STEP_NODE_BASE_CLICK_OUTSIDE_ID } from '@/workflow/wor
import { workflowSelectedNodeComponentState } from '@/workflow/workflow-diagram/states/workflowSelectedNodeComponentState';
import { type WorkflowDiagramStepNodeData } from '@/workflow/workflow-diagram/types/WorkflowDiagram';
import { getWorkflowNodeIconKey } from '@/workflow/workflow-diagram/utils/getWorkflowNodeIconKey';
+import { WorkflowDiagramHandleSource } from '@/workflow/workflow-diagram/workflow-nodes/components/WorkflowDiagramHandleSource';
+import { WorkflowDiagramHandleTarget } from '@/workflow/workflow-diagram/workflow-nodes/components/WorkflowDiagramHandleTarget';
import { WorkflowDiagramStepNodeIcon } from '@/workflow/workflow-diagram/workflow-nodes/components/WorkflowDiagramStepNodeIcon';
import { WorkflowNodeContainer } from '@/workflow/workflow-diagram/workflow-nodes/components/WorkflowNodeContainer';
import { WorkflowNodeIconContainer } from '@/workflow/workflow-diagram/workflow-nodes/components/WorkflowNodeIconContainer';
@@ -16,12 +18,11 @@ import { WorkflowNodeLabel } from '@/workflow/workflow-diagram/workflow-nodes/co
import { WorkflowNodeLabelWithCounterPart } from '@/workflow/workflow-diagram/workflow-nodes/components/WorkflowNodeLabelWithCounterPart';
import { WorkflowNodeRightPart } from '@/workflow/workflow-diagram/workflow-nodes/components/WorkflowNodeRightPart';
import { WorkflowNodeTitle } from '@/workflow/workflow-diagram/workflow-nodes/components/WorkflowNodeTitle';
+import { Position } from '@xyflow/react';
import { useContext } from 'react';
import { useSetRecoilState } from 'recoil';
import { capitalize, isDefined } from 'twenty-shared/utils';
import { useIcons } from 'twenty-ui/display';
-import { WorkflowDiagramHandleTarget } from '@/workflow/workflow-diagram/workflow-nodes/components/WorkflowDiagramHandleTarget';
-import { WorkflowDiagramHandleSource } from '@/workflow/workflow-diagram/workflow-nodes/components/WorkflowDiagramHandleSource';
export const WorkflowDiagramStepNodeReadonly = ({
id,
@@ -95,7 +96,12 @@ export const WorkflowDiagramStepNodeReadonly = ({
-
+
>
);
};
diff --git a/packages/twenty-front/src/modules/workflow/workflow-diagram/workflow-nodes/components/WorkflowRunDiagramStepNode.tsx b/packages/twenty-front/src/modules/workflow/workflow-diagram/workflow-nodes/components/WorkflowRunDiagramStepNode.tsx
index 6d14430f8b..3b77100630 100644
--- a/packages/twenty-front/src/modules/workflow/workflow-diagram/workflow-nodes/components/WorkflowRunDiagramStepNode.tsx
+++ b/packages/twenty-front/src/modules/workflow/workflow-diagram/workflow-nodes/components/WorkflowRunDiagramStepNode.tsx
@@ -9,6 +9,8 @@ import { WORKFLOW_DIAGRAM_STEP_NODE_BASE_CLICK_OUTSIDE_ID } from '@/workflow/wor
import { workflowSelectedNodeComponentState } from '@/workflow/workflow-diagram/states/workflowSelectedNodeComponentState';
import { type WorkflowRunDiagramStepNodeData } from '@/workflow/workflow-diagram/types/WorkflowDiagram';
import { getWorkflowNodeIconKey } from '@/workflow/workflow-diagram/utils/getWorkflowNodeIconKey';
+import { WorkflowDiagramHandleSource } from '@/workflow/workflow-diagram/workflow-nodes/components/WorkflowDiagramHandleSource';
+import { WorkflowDiagramHandleTarget } from '@/workflow/workflow-diagram/workflow-nodes/components/WorkflowDiagramHandleTarget';
import { WorkflowDiagramStepNodeIcon } from '@/workflow/workflow-diagram/workflow-nodes/components/WorkflowDiagramStepNodeIcon';
import { WorkflowNodeContainer } from '@/workflow/workflow-diagram/workflow-nodes/components/WorkflowNodeContainer';
import { WorkflowNodeIconContainer } from '@/workflow/workflow-diagram/workflow-nodes/components/WorkflowNodeIconContainer';
@@ -18,14 +20,13 @@ import { WorkflowNodeRightPart } from '@/workflow/workflow-diagram/workflow-node
import { WorkflowNodeTitle } from '@/workflow/workflow-diagram/workflow-nodes/components/WorkflowNodeTitle';
import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
+import { Position } from '@xyflow/react';
import { useContext } from 'react';
import { useSetRecoilState } from 'recoil';
import { capitalize, isDefined } from 'twenty-shared/utils';
import { StepStatus } from 'twenty-shared/workflow';
import { IconCheck, IconX, useIcons } from 'twenty-ui/display';
import { Loader } from 'twenty-ui/feedback';
-import { WorkflowDiagramHandleTarget } from '@/workflow/workflow-diagram/workflow-nodes/components/WorkflowDiagramHandleTarget';
-import { WorkflowDiagramHandleSource } from '@/workflow/workflow-diagram/workflow-nodes/components/WorkflowDiagramHandleSource';
const StyledNodeLabelWithCounterPart = styled(WorkflowNodeLabelWithCounterPart)`
column-gap: ${({ theme }) => theme.spacing(2)};
@@ -151,9 +152,11 @@ export const WorkflowRunDiagramStepNode = ({
>
);
diff --git a/packages/twenty-front/src/modules/workflow/workflow-diagram/workflow-nodes/constants/WorkflowDiagramIteratorNodeLoopHandleId.ts b/packages/twenty-front/src/modules/workflow/workflow-diagram/workflow-nodes/constants/WorkflowDiagramIteratorNodeLoopHandleId.ts
new file mode 100644
index 0000000000..c210d4f09d
--- /dev/null
+++ b/packages/twenty-front/src/modules/workflow/workflow-diagram/workflow-nodes/constants/WorkflowDiagramIteratorNodeLoopHandleId.ts
@@ -0,0 +1 @@
+export const WORKFLOW_DIAGRAM_ITERATOR_NODE_LOOP_HANDLE_ID = 'loop';
diff --git a/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/hooks/useActionHeaderTypeOrThrow.ts b/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/hooks/useActionHeaderTypeOrThrow.ts
index a774a3cb7f..3debfd5b1b 100644
--- a/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/hooks/useActionHeaderTypeOrThrow.ts
+++ b/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/hooks/useActionHeaderTypeOrThrow.ts
@@ -1,9 +1,9 @@
import { type WorkflowActionType } from '@/workflow/types/Workflow';
import { getActionHeaderTypeOrThrow } from '@/workflow/workflow-steps/workflow-actions/utils/getActionHeaderTypeOrThrow';
-import { useLingui } from '@lingui/react';
+import { useLingui } from '@lingui/react/macro';
export const useActionHeaderTypeOrThrow = (actionType: WorkflowActionType) => {
- const { _ } = useLingui();
+ const { i18n } = useLingui();
- return _(getActionHeaderTypeOrThrow(actionType));
+ return i18n._(getActionHeaderTypeOrThrow(actionType));
};