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)); };