From dff5e3cd7b6c0f0f0ff33cad1a743c771940dcad Mon Sep 17 00:00:00 2001 From: Abdul Rahman <81605929+abdulrahmancodes@users.noreply.github.com> Date: Thu, 1 Jan 2026 19:35:56 +0530 Subject: [PATCH] feat: Add If/Else node (#16833) Closes [#1265](https://github.com/twentyhq/core-team-issues/issues/1265) --- .../src/generated-metadata/graphql.ts | 2 + .../twenty-front/src/generated/graphql.ts | 2 + .../CommandMenuWorkflowSelectAction.tsx | 7 +- .../src/modules/workflow/types/Workflow.ts | 3 + .../generateNodesAndEdgesForIfElseNode.ts | 117 +++++++ .../utils/generateWorkflowDiagram.ts | 18 ++ .../WorkflowDiagramDefaultEdgeEditable.tsx | 67 ++-- .../WorkflowDiagramDefaultEdgeReadonly.tsx | 7 +- .../WorkflowDiagramEdgeLabelContainer.tsx | 11 +- .../WorkflowDropdownStepOutputItems.tsx | 4 +- .../components/WorkflowRunStepNodeDetail.tsx | 12 + .../components/WorkflowStepDetail.tsx | 10 + .../WorkflowEditActionFilterBodyEffect.tsx | 8 +- .../WorkflowStepFilterAddFilterRuleSelect.tsx | 4 +- ...kflowStepFilterAddRootStepFilterButton.tsx | 4 +- .../components/WorkflowStepFilterColumn.tsx | 45 ++- .../WorkflowStepFilterFieldSelect.tsx | 4 +- .../WorkflowStepFilterGroupChildren.tsx | 8 +- .../WorkflowStepFilterGroupColumn.tsx | 8 +- ...WorkflowStepFilterGroupOptionsDropdown.tsx | 4 +- .../WorkflowStepFilterLogicalOperatorCell.tsx | 11 +- .../WorkflowStepFilterOperandSelect.tsx | 6 +- .../WorkflowStepFilterOptionsDropdown.tsx | 4 +- .../WorkflowStepFilterValueCompositeInput.tsx | 2 +- .../WorkflowStepFilterValueInput.tsx | 6 +- ...wStepFilterAddFilterRuleSelect.stories.tsx | 2 +- ...pFilterAddRootStepFilterButton.stories.tsx | 2 +- .../WorkflowStepFilterColumn.stories.tsx | 2 +- .../WorkflowStepFilterFieldSelect.stories.tsx | 2 +- ...wStepFilterLogicalOperatorCell.stories.tsx | 2 +- ...orkflowStepFilterOperandSelect.stories.tsx | 2 +- .../WorkflowStepFilterValueInput.stories.tsx | 2 +- .../hooks/useAddRootStepFilter.ts | 10 +- ...hildStepFiltersAndChildStepFilterGroups.ts | 4 +- .../hooks/useFilterCounter.ts | 0 .../hooks/useRemoveStepFilter.ts | 6 +- .../hooks/useRemoveStepFilterGroup.ts | 6 +- .../hooks/useUpsertStepFilterSettings.ts | 6 +- ...tepFilterGroupsComponentInstanceContext.ts | 0 .../StepFiltersComponentInstanceContext.ts | 0 .../context/WorkflowStepFilterContext.ts | 6 +- .../currentStepFilterGroupsComponentState.ts | 2 +- .../currentStepFiltersComponentState.ts | 2 +- ...entStepFilterGroupsComponentFamilyState.ts | 2 +- ...dCurrentStepFiltersComponentFamilyState.ts | 2 +- ...otLevelStepFilterGroupComponentSelector.ts | 4 +- .../filters/types/FilterSettings.ts | 6 + .../utils/getStepFilterOperands.ts | 0 .../isStepFilterGroupChildAStepFilterGroup.ts | 0 .../workflow-steps/hooks/useCreateStep.ts | 11 +- .../workflow-steps/hooks/useDeleteStep.ts | 38 ++- .../workflow-actions/constants/FlowActions.ts | 8 +- .../constants/actions/IfElseAction.ts | 11 + .../components/WorkflowEditActionFilter.tsx | 12 +- .../WorkflowEditActionFilterBody.tsx | 18 +- .../WorkflowStepFilterDecorator.tsx | 4 +- .../components/WorkflowEditActionIfElse.tsx | 52 +++ .../WorkflowEditActionIfElseBody.tsx | 299 ++++++++++++++++++ .../components/WorkflowIfElseBranchEditor.tsx | 102 ++++++ .../utils/calculateElseIfBranchPosition.ts | 30 ++ .../utils/calculateExistingBranchPositions.ts | 41 +++ .../utils/createElseIfBranch.ts | 37 +++ .../if-else-action/utils/getBranchLabel.ts | 28 ++ .../utils/getBranchesToDelete.ts | 27 ++ .../utils/getEmptyChildStepIds.ts | 21 ++ .../workflow-actions/utils/getActionIcon.ts | 1 + .../utils/getActionIconColorOrThrow.ts | 1 + .../hooks/useTidyUpWorkflowVersion.ts | 2 +- .../constants/public-feature-flag.const.ts | 8 + .../enums/feature-flag-key.enum.ts | 1 + .../workspace-entity-manager.spec.ts | 1 + .../core/utils/seed-feature-flags.util.ts | 5 + ...rsion-step-operations.workspace-service.ts | 167 +++++++++- .../factories/workflow-action.factory.ts | 4 + .../is-workflow-if-else-action.guard.ts | 9 + .../if-else/if-else-action.module.ts | 9 + .../if-else/if-else.workflow-action.ts | 68 ++++ .../workflow-if-else-action-settings.type.ts | 12 + .../types/workflow-if-else-result.type.ts | 3 + .../utils/find-matching-branch.util.ts | 77 +++++ .../types/workflow-action-settings.type.ts | 2 + .../types/workflow-action-type.enum.ts | 1 + .../types/workflow-action.type.ts | 7 + .../workflow-executor.module.ts | 2 + .../workflow-executor.workspace-service.ts | 16 + .../constants/IfElseBranchPositionOffsets.ts | 10 + packages/twenty-shared/src/workflow/index.ts | 9 + .../schemas/filter-action-settings-schema.ts | 30 +- .../workflow/schemas/if-else-action-schema.ts | 8 + .../schemas/if-else-action-settings-schema.ts | 19 ++ .../schemas/step-filter-group-schema.ts | 9 + .../workflow/schemas/step-filter-schema.ts | 15 + .../schemas/workflow-action-schema.ts | 2 + .../src/workflow/types/StepIfElseBranch.ts | 5 + .../public/images/lab/is-if-else-enabled.png | Bin 0 -> 145941 bytes 95 files changed, 1523 insertions(+), 163 deletions(-) create mode 100644 packages/twenty-front/src/modules/workflow/workflow-diagram/utils/generateNodesAndEdgesForIfElseNode.ts rename packages/twenty-front/src/modules/workflow/workflow-steps/{workflow-actions/filter-action => }/components/WorkflowDropdownStepOutputItems.tsx (98%) rename packages/twenty-front/src/modules/workflow/workflow-steps/{workflow-actions/filter-action => filters}/components/WorkflowEditActionFilterBodyEffect.tsx (86%) rename packages/twenty-front/src/modules/workflow/workflow-steps/{workflow-actions/filter-action => filters}/components/WorkflowStepFilterAddFilterRuleSelect.tsx (95%) rename packages/twenty-front/src/modules/workflow/workflow-steps/{workflow-actions/filter-action => filters}/components/WorkflowStepFilterAddRootStepFilterButton.tsx (86%) rename packages/twenty-front/src/modules/workflow/workflow-steps/{workflow-actions/filter-action => filters}/components/WorkflowStepFilterColumn.tsx (50%) rename packages/twenty-front/src/modules/workflow/workflow-steps/{workflow-actions/filter-action => filters}/components/WorkflowStepFilterFieldSelect.tsx (96%) rename packages/twenty-front/src/modules/workflow/workflow-steps/{workflow-actions/filter-action => filters}/components/WorkflowStepFilterGroupChildren.tsx (84%) rename packages/twenty-front/src/modules/workflow/workflow-steps/{workflow-actions/filter-action => filters}/components/WorkflowStepFilterGroupColumn.tsx (81%) rename packages/twenty-front/src/modules/workflow/workflow-steps/{workflow-actions/filter-action => filters}/components/WorkflowStepFilterGroupOptionsDropdown.tsx (92%) rename packages/twenty-front/src/modules/workflow/workflow-steps/{workflow-actions/filter-action => filters}/components/WorkflowStepFilterLogicalOperatorCell.tsx (91%) rename packages/twenty-front/src/modules/workflow/workflow-steps/{workflow-actions/filter-action => filters}/components/WorkflowStepFilterOperandSelect.tsx (92%) rename packages/twenty-front/src/modules/workflow/workflow-steps/{workflow-actions/filter-action => filters}/components/WorkflowStepFilterOptionsDropdown.tsx (89%) rename packages/twenty-front/src/modules/workflow/workflow-steps/{workflow-actions/filter-action => filters}/components/WorkflowStepFilterValueCompositeInput.tsx (98%) rename packages/twenty-front/src/modules/workflow/workflow-steps/{workflow-actions/filter-action => filters}/components/WorkflowStepFilterValueInput.tsx (97%) rename packages/twenty-front/src/modules/workflow/workflow-steps/{workflow-actions/filter-action => filters}/components/__stories__/WorkflowStepFilterAddFilterRuleSelect.stories.tsx (94%) rename packages/twenty-front/src/modules/workflow/workflow-steps/{workflow-actions/filter-action => filters}/components/__stories__/WorkflowStepFilterAddRootStepFilterButton.stories.tsx (93%) rename packages/twenty-front/src/modules/workflow/workflow-steps/{workflow-actions/filter-action => filters}/components/__stories__/WorkflowStepFilterColumn.stories.tsx (96%) rename packages/twenty-front/src/modules/workflow/workflow-steps/{workflow-actions/filter-action => filters}/components/__stories__/WorkflowStepFilterFieldSelect.stories.tsx (95%) rename packages/twenty-front/src/modules/workflow/workflow-steps/{workflow-actions/filter-action => filters}/components/__stories__/WorkflowStepFilterLogicalOperatorCell.stories.tsx (95%) rename packages/twenty-front/src/modules/workflow/workflow-steps/{workflow-actions/filter-action => filters}/components/__stories__/WorkflowStepFilterOperandSelect.stories.tsx (96%) rename packages/twenty-front/src/modules/workflow/workflow-steps/{workflow-actions/filter-action => filters}/components/__stories__/WorkflowStepFilterValueInput.stories.tsx (96%) rename packages/twenty-front/src/modules/workflow/workflow-steps/{workflow-actions/filter-action => filters}/hooks/useAddRootStepFilter.ts (84%) rename packages/twenty-front/src/modules/workflow/workflow-steps/{workflow-actions/filter-action => filters}/hooks/useChildStepFiltersAndChildStepFilterGroups.ts (91%) rename packages/twenty-front/src/modules/workflow/workflow-steps/{workflow-actions/filter-action => filters}/hooks/useFilterCounter.ts (100%) rename packages/twenty-front/src/modules/workflow/workflow-steps/{workflow-actions/filter-action => filters}/hooks/useRemoveStepFilter.ts (93%) rename packages/twenty-front/src/modules/workflow/workflow-steps/{workflow-actions/filter-action => filters}/hooks/useRemoveStepFilterGroup.ts (91%) rename packages/twenty-front/src/modules/workflow/workflow-steps/{workflow-actions/filter-action => filters}/hooks/useUpsertStepFilterSettings.ts (92%) rename packages/twenty-front/src/modules/workflow/workflow-steps/{workflow-actions/filter-action => filters}/states/context/StepFilterGroupsComponentInstanceContext.ts (100%) rename packages/twenty-front/src/modules/workflow/workflow-steps/{workflow-actions/filter-action => filters}/states/context/StepFiltersComponentInstanceContext.ts (100%) rename packages/twenty-front/src/modules/workflow/workflow-steps/{workflow-actions/filter-action => filters}/states/context/WorkflowStepFilterContext.ts (56%) rename packages/twenty-front/src/modules/workflow/workflow-steps/{workflow-actions/filter-action => filters}/states/currentStepFilterGroupsComponentState.ts (81%) rename packages/twenty-front/src/modules/workflow/workflow-steps/{workflow-actions/filter-action => filters}/states/currentStepFiltersComponentState.ts (81%) rename packages/twenty-front/src/modules/workflow/workflow-steps/{workflow-actions/filter-action => filters}/states/hasInitializedCurrentStepFilterGroupsComponentFamilyState.ts (81%) rename packages/twenty-front/src/modules/workflow/workflow-steps/{workflow-actions/filter-action => filters}/states/hasInitializedCurrentStepFiltersComponentFamilyState.ts (82%) rename packages/twenty-front/src/modules/workflow/workflow-steps/{workflow-actions/filter-action => filters}/states/rootLevelStepFilterGroupComponentSelector.ts (81%) create mode 100644 packages/twenty-front/src/modules/workflow/workflow-steps/filters/types/FilterSettings.ts rename packages/twenty-front/src/modules/workflow/workflow-steps/{workflow-actions/filter-action => filters}/utils/getStepFilterOperands.ts (100%) rename packages/twenty-front/src/modules/workflow/workflow-steps/{workflow-actions/filter-action => filters}/utils/isStepFilterGroupChildAStepFilterGroup.ts (100%) create mode 100644 packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/constants/actions/IfElseAction.ts create mode 100644 packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/if-else-action/components/WorkflowEditActionIfElse.tsx create mode 100644 packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/if-else-action/components/WorkflowEditActionIfElseBody.tsx create mode 100644 packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/if-else-action/components/WorkflowIfElseBranchEditor.tsx create mode 100644 packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/if-else-action/utils/calculateElseIfBranchPosition.ts create mode 100644 packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/if-else-action/utils/calculateExistingBranchPositions.ts create mode 100644 packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/if-else-action/utils/createElseIfBranch.ts create mode 100644 packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/if-else-action/utils/getBranchLabel.ts create mode 100644 packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/if-else-action/utils/getBranchesToDelete.ts create mode 100644 packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/if-else-action/utils/getEmptyChildStepIds.ts create mode 100644 packages/twenty-server/src/modules/workflow/workflow-executor/workflow-actions/if-else/guards/is-workflow-if-else-action.guard.ts create mode 100644 packages/twenty-server/src/modules/workflow/workflow-executor/workflow-actions/if-else/if-else-action.module.ts create mode 100644 packages/twenty-server/src/modules/workflow/workflow-executor/workflow-actions/if-else/if-else.workflow-action.ts create mode 100644 packages/twenty-server/src/modules/workflow/workflow-executor/workflow-actions/if-else/types/workflow-if-else-action-settings.type.ts create mode 100644 packages/twenty-server/src/modules/workflow/workflow-executor/workflow-actions/if-else/types/workflow-if-else-result.type.ts create mode 100644 packages/twenty-server/src/modules/workflow/workflow-executor/workflow-actions/if-else/utils/find-matching-branch.util.ts create mode 100644 packages/twenty-shared/src/workflow/constants/IfElseBranchPositionOffsets.ts create mode 100644 packages/twenty-shared/src/workflow/schemas/if-else-action-schema.ts create mode 100644 packages/twenty-shared/src/workflow/schemas/if-else-action-settings-schema.ts create mode 100644 packages/twenty-shared/src/workflow/schemas/step-filter-group-schema.ts create mode 100644 packages/twenty-shared/src/workflow/schemas/step-filter-schema.ts create mode 100644 packages/twenty-shared/src/workflow/types/StepIfElseBranch.ts create mode 100644 packages/twenty-website/public/images/lab/is-if-else-enabled.png diff --git a/packages/twenty-front/src/generated-metadata/graphql.ts b/packages/twenty-front/src/generated-metadata/graphql.ts index ff77e5c050..0f081a0a51 100644 --- a/packages/twenty-front/src/generated-metadata/graphql.ts +++ b/packages/twenty-front/src/generated-metadata/graphql.ts @@ -1343,6 +1343,7 @@ export enum FeatureFlagKey { IS_APPLICATION_ENABLED = 'IS_APPLICATION_ENABLED', IS_DASHBOARD_V2_ENABLED = 'IS_DASHBOARD_V2_ENABLED', IS_EMAILING_DOMAIN_ENABLED = 'IS_EMAILING_DOMAIN_ENABLED', + IS_IF_ELSE_ENABLED = 'IS_IF_ELSE_ENABLED', IS_JSON_FILTER_ENABLED = 'IS_JSON_FILTER_ENABLED', IS_PAGE_LAYOUT_ENABLED = 'IS_PAGE_LAYOUT_ENABLED', IS_POSTGRESQL_INTEGRATION_ENABLED = 'IS_POSTGRESQL_INTEGRATION_ENABLED', @@ -5010,6 +5011,7 @@ export enum WorkflowActionType { FIND_RECORDS = 'FIND_RECORDS', FORM = 'FORM', HTTP_REQUEST = 'HTTP_REQUEST', + IF_ELSE = 'IF_ELSE', ITERATOR = 'ITERATOR', SEND_EMAIL = 'SEND_EMAIL', UPDATE_RECORD = 'UPDATE_RECORD', diff --git a/packages/twenty-front/src/generated/graphql.ts b/packages/twenty-front/src/generated/graphql.ts index adbefbfbfd..2529543053 100644 --- a/packages/twenty-front/src/generated/graphql.ts +++ b/packages/twenty-front/src/generated/graphql.ts @@ -1326,6 +1326,7 @@ export enum FeatureFlagKey { IS_APPLICATION_ENABLED = 'IS_APPLICATION_ENABLED', IS_DASHBOARD_V2_ENABLED = 'IS_DASHBOARD_V2_ENABLED', IS_EMAILING_DOMAIN_ENABLED = 'IS_EMAILING_DOMAIN_ENABLED', + IS_IF_ELSE_ENABLED = 'IS_IF_ELSE_ENABLED', IS_JSON_FILTER_ENABLED = 'IS_JSON_FILTER_ENABLED', IS_PAGE_LAYOUT_ENABLED = 'IS_PAGE_LAYOUT_ENABLED', IS_POSTGRESQL_INTEGRATION_ENABLED = 'IS_POSTGRESQL_INTEGRATION_ENABLED', @@ -4829,6 +4830,7 @@ export enum WorkflowActionType { FIND_RECORDS = 'FIND_RECORDS', FORM = 'FORM', HTTP_REQUEST = 'HTTP_REQUEST', + IF_ELSE = 'IF_ELSE', ITERATOR = 'ITERATOR', SEND_EMAIL = 'SEND_EMAIL', UPDATE_RECORD = 'UPDATE_RECORD', diff --git a/packages/twenty-front/src/modules/command-menu/pages/workflow/action/components/CommandMenuWorkflowSelectAction.tsx b/packages/twenty-front/src/modules/command-menu/pages/workflow/action/components/CommandMenuWorkflowSelectAction.tsx index b2c9a87640..0aff503a28 100644 --- a/packages/twenty-front/src/modules/command-menu/pages/workflow/action/components/CommandMenuWorkflowSelectAction.tsx +++ b/packages/twenty-front/src/modules/command-menu/pages/workflow/action/components/CommandMenuWorkflowSelectAction.tsx @@ -17,6 +17,9 @@ export const CommandMenuWorkflowSelectAction = ({ onActionSelected: (actionType: WorkflowActionType) => void; }) => { const isAiEnabled = useIsFeatureEnabled(FeatureFlagKey.IS_AI_ENABLED); + const isIfElseEnabled = useIsFeatureEnabled( + FeatureFlagKey.IS_IF_ELSE_ENABLED, + ); const { t } = useLingui(); @@ -46,7 +49,9 @@ export const CommandMenuWorkflowSelectAction = ({ {t`Flow`} action.type !== 'IF_ELSE' || isIfElseEnabled, + )} onClick={onActionSelected} /> diff --git a/packages/twenty-front/src/modules/workflow/types/Workflow.ts b/packages/twenty-front/src/modules/workflow/types/Workflow.ts index a0fabeb719..6885c89c8d 100644 --- a/packages/twenty-front/src/modules/workflow/types/Workflow.ts +++ b/packages/twenty-front/src/modules/workflow/types/Workflow.ts @@ -13,6 +13,7 @@ import { type workflowFindRecordsActionSchema, type workflowFormActionSchema, type workflowHttpRequestActionSchema, + type workflowIfElseActionSchema, type workflowIteratorActionSchema, type workflowManualTriggerSchema, type workflowRunSchema, @@ -50,6 +51,7 @@ export type WorkflowFindRecordsAction = z.infer< export type WorkflowDelayAction = z.infer; export type WorkflowFilterAction = z.infer; export type WorkflowFormAction = z.infer; +export type WorkflowIfElseAction = z.infer; export type WorkflowHttpRequestAction = z.infer< typeof workflowHttpRequestActionSchema >; @@ -68,6 +70,7 @@ export type WorkflowAction = | WorkflowUpsertRecordAction | WorkflowFindRecordsAction | WorkflowFilterAction + | WorkflowIfElseAction | WorkflowFormAction | WorkflowHttpRequestAction | WorkflowAiAgentAction diff --git a/packages/twenty-front/src/modules/workflow/workflow-diagram/utils/generateNodesAndEdgesForIfElseNode.ts b/packages/twenty-front/src/modules/workflow/workflow-diagram/utils/generateNodesAndEdgesForIfElseNode.ts new file mode 100644 index 0000000000..153d4c9f0f --- /dev/null +++ b/packages/twenty-front/src/modules/workflow/workflow-diagram/utils/generateNodesAndEdgesForIfElseNode.ts @@ -0,0 +1,117 @@ +import { + type WorkflowIfElseAction, + type WorkflowStep, +} from '@/workflow/types/Workflow'; +import { type WorkflowContext } from '@/workflow/workflow-diagram/types/WorkflowContext'; +import { + type WorkflowDiagramEdge, + type WorkflowDiagramNode, + type WorkflowDiagramStepNodeData, +} from '@/workflow/workflow-diagram/types/WorkflowDiagram'; +import { getEdgePathStrategy } from '@/workflow/workflow-diagram/utils/getEdgePathStrategy'; +import { getEdgeTypeBetweenTwoNodes } from '@/workflow/workflow-diagram/utils/getEdgeTypeBetweenTwoNodes'; +import { WORKFLOW_VISUALIZER_EDGE_DEFAULT_CONFIGURATION } from '@/workflow/workflow-diagram/workflow-edges/constants/WorkflowVisualizerEdgeDefaultConfiguration'; +import { WORKFLOW_DIAGRAM_NODE_DEFAULT_SOURCE_HANDLE_ID } from '@/workflow/workflow-diagram/workflow-nodes/constants/WorkflowDiagramNodeDefaultSourceHandleId'; +import { WORKFLOW_DIAGRAM_NODE_DEFAULT_TARGET_HANDLE_ID } from '@/workflow/workflow-diagram/workflow-nodes/constants/WorkflowDiagramNodeDefaultTargetHandleId'; +import { getBranchLabel } from '@/workflow/workflow-steps/workflow-actions/if-else-action/utils/getBranchLabel'; +import { Position } from '@xyflow/react'; +import { isDefined } from 'twenty-shared/utils'; +import { v4 } from 'uuid'; + +export const generateNodesAndEdgesForIfElseNode = ({ + step, + steps, + xPos, + yPos, + nodes, + edges, + workflowContext, +}: { + step: WorkflowIfElseAction; + steps: 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]; + + const ifElseNode: WorkflowDiagramNode = { + id: step.id, + data: { + nodeType: 'action', + actionType: step.type, + name: step.name, + hasNextStepIds: true, + stepId: step.id, + position: step.position ?? { + x: xPos, + y: yPos, + }, + } satisfies WorkflowDiagramStepNodeData, + position: step.position ?? { + x: xPos, + y: yPos, + }, + }; + + updatedNodes.push(ifElseNode); + + const branches = step.settings.input.branches; + const totalBranches = branches.length; + + branches.forEach((branch, branchIndex) => { + const label = getBranchLabel({ + branchIndex, + totalBranches, + branch, + }); + + const nextStepIds = branch.nextStepIds; + for (const nextStepId of nextStepIds) { + const nextStep = steps.find((s) => s.id === nextStepId); + if (!isDefined(nextStep)) { + continue; + } + + updatedEdges.push({ + ...WORKFLOW_VISUALIZER_EDGE_DEFAULT_CONFIGURATION, + type: edgeTypeBetweenTwoNodes, + id: v4(), + source: step.id, + sourceHandle: WORKFLOW_DIAGRAM_NODE_DEFAULT_SOURCE_HANDLE_ID, + target: nextStepId, + deletable: false, + selectable: false, + reconnectable: false, + targetHandle: WORKFLOW_DIAGRAM_NODE_DEFAULT_TARGET_HANDLE_ID, + data: { + ...WORKFLOW_VISUALIZER_EDGE_DEFAULT_CONFIGURATION.data, + labelOptions: { + position: Position.Bottom, + label, + }, + edgePathStrategy: getEdgePathStrategy({ + step, + steps, + nextStepId, + }), + }, + }); + } + }); + + 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 09519fe302..78b7ca10eb 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 @@ -12,6 +12,7 @@ import { type WorkflowDiagramNode, } from '@/workflow/workflow-diagram/types/WorkflowDiagram'; import { generateNodesAndEdgesForDefaultNode } from '@/workflow/workflow-diagram/utils/generateNodesAndEdgesForDefaultNode'; +import { generateNodesAndEdgesForIfElseNode } from '@/workflow/workflow-diagram/utils/generateNodesAndEdgesForIfElseNode'; import { generateNodesAndEdgesForIteratorNode } from '@/workflow/workflow-diagram/utils/generateNodesAndEdgesForIteratorNode'; import { getEdgeTypeBetweenTwoNodes } from '@/workflow/workflow-diagram/utils/getEdgeTypeBetweenTwoNodes'; import { getWorkflowDiagramTriggerNode } from '@/workflow/workflow-diagram/utils/getWorkflowDiagramTriggerNode'; @@ -87,6 +88,23 @@ export const generateWorkflowDiagram = ({ break; } + case 'IF_ELSE': { + const { nodes: ifElseNodes, edges: ifElseEdges } = + generateNodesAndEdgesForIfElseNode({ + step, + steps, + xPos, + yPos: levelYPos, + nodes, + edges, + workflowContext, + }); + + nodes = ifElseNodes; + edges = ifElseEdges; + + break; + } default: { const { nodes: defaultNodes, edges: defaultEdges } = generateNodesAndEdgesForDefaultNode({ 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 7facd18aae..18ce5172d2 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 @@ -34,11 +34,14 @@ export const WorkflowDiagramDefaultEdgeEditable = ({ markerStart, markerEnd, data, + deletable, }: WorkflowDiagramDefaultEdgeEditableProps) => { const { i18n } = useLingui(); const { isEdgeHovered } = useEdgeState(); + const isEditable = deletable !== false; + const { segments, overlayPosition: [labelX, labelY], @@ -107,42 +110,48 @@ export const WorkflowDiagramDefaultEdgeEditable = ({ sourceX={sourceX} sourceY={sourceY} position={data.labelOptions.position} + centerX={labelX} + centerY={labelY} > )} - - - - - + + + + + )} ); diff --git a/packages/twenty-front/src/modules/workflow/workflow-diagram/workflow-edges/components/WorkflowDiagramDefaultEdgeReadonly.tsx b/packages/twenty-front/src/modules/workflow/workflow-diagram/workflow-edges/components/WorkflowDiagramDefaultEdgeReadonly.tsx index 21fe59ee68..5388e9848b 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-diagram/workflow-edges/components/WorkflowDiagramDefaultEdgeReadonly.tsx +++ b/packages/twenty-front/src/modules/workflow/workflow-diagram/workflow-edges/components/WorkflowDiagramDefaultEdgeReadonly.tsx @@ -23,7 +23,10 @@ export const WorkflowDiagramDefaultEdgeReadonly = ({ }: WorkflowDiagramDefaultEdgeReadonlyProps) => { const theme = useTheme(); - const { segments } = getEdgePath({ + const { + segments, + overlayPosition: [labelX, labelY], + } = getEdgePath({ sourceX, sourceY, sourcePosition, @@ -53,6 +56,8 @@ export const WorkflowDiagramDefaultEdgeReadonly = ({ sourceX={sourceX} sourceY={sourceY} position={data.labelOptions.position} + centerX={labelX} + centerY={labelY} > 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 index 80129c99ed..efb9590a18 100644 --- 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 @@ -1,16 +1,25 @@ import { css } from '@emotion/react'; import styled from '@emotion/styled'; import { Position } from '@xyflow/react'; +import { isDefined } from 'twenty-shared/utils'; const StyledWorkflowDiagramEdgeLabelContainer = styled.div<{ sourceX: number; sourceY: number; position: Position; + centerX?: number; + centerY?: number; }>` position: absolute; width: fit-content; - ${({ position, sourceX, sourceY }) => { + ${({ position, sourceX, sourceY, centerX, centerY }) => { + if (isDefined(centerX) && isDefined(centerY)) { + return css` + transform: translate(-50%, -50%) translate(${centerX}px, ${centerY}px); + `; + } + switch (position) { case Position.Right: { return css` diff --git a/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/filter-action/components/WorkflowDropdownStepOutputItems.tsx b/packages/twenty-front/src/modules/workflow/workflow-steps/components/WorkflowDropdownStepOutputItems.tsx similarity index 98% rename from packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/filter-action/components/WorkflowDropdownStepOutputItems.tsx rename to packages/twenty-front/src/modules/workflow/workflow-steps/components/WorkflowDropdownStepOutputItems.tsx index 974282c402..df56fab5ca 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/filter-action/components/WorkflowDropdownStepOutputItems.tsx +++ b/packages/twenty-front/src/modules/workflow/workflow-steps/components/WorkflowDropdownStepOutputItems.tsx @@ -10,8 +10,8 @@ import { DropdownMenuHeaderLeftComponent } from '@/ui/layout/dropdown/components import { GenericDropdownContentWidth } from '@/ui/layout/dropdown/constants/GenericDropdownContentWidth'; import { useWorkflowVersionIdOrThrow } from '@/workflow/hooks/useWorkflowVersionIdOrThrow'; import { stepsOutputSchemaFamilySelector } from '@/workflow/states/selectors/stepsOutputSchemaFamilySelector'; -import { useUpsertStepFilterSettings } from '@/workflow/workflow-steps/workflow-actions/filter-action/hooks/useUpsertStepFilterSettings'; -import { getStepFilterOperands } from '@/workflow/workflow-steps/workflow-actions/filter-action/utils/getStepFilterOperands'; +import { useUpsertStepFilterSettings } from '@/workflow/workflow-steps/filters/hooks/useUpsertStepFilterSettings'; +import { getStepFilterOperands } from '@/workflow/workflow-steps/filters/utils/getStepFilterOperands'; import { useVariableDropdown } from '@/workflow/workflow-variables/hooks/useVariableDropdown'; import { isRecordOutputSchemaV2 } from '@/workflow/workflow-variables/types/guards/isRecordOutputSchemaV2'; import { type StepOutputSchemaV2 } from '@/workflow/workflow-variables/types/StepOutputSchemaV2'; diff --git a/packages/twenty-front/src/modules/workflow/workflow-steps/components/WorkflowRunStepNodeDetail.tsx b/packages/twenty-front/src/modules/workflow/workflow-steps/components/WorkflowRunStepNodeDetail.tsx index 964fa0e3da..f820ff250a 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-steps/components/WorkflowRunStepNodeDetail.tsx +++ b/packages/twenty-front/src/modules/workflow/workflow-steps/components/WorkflowRunStepNodeDetail.tsx @@ -15,6 +15,7 @@ import { WorkflowEditActionUpsertRecord } from '@/workflow/workflow-steps/workfl import { WorkflowEditActionDelay } from '@/workflow/workflow-steps/workflow-actions/delay-actions/components/WorkflowEditActionDelay'; import { WorkflowEditActionFilter } from '@/workflow/workflow-steps/workflow-actions/filter-action/components/WorkflowEditActionFilter'; import { WorkflowEditActionFindRecords } from '@/workflow/workflow-steps/workflow-actions/find-records-action/components/WorkflowEditActionFindRecords'; +import { WorkflowEditActionIfElse } from '@/workflow/workflow-steps/workflow-actions/if-else-action/components/WorkflowEditActionIfElse'; import { WorkflowEditActionFormFiller } from '@/workflow/workflow-steps/workflow-actions/form-action/components/WorkflowEditActionFormFiller'; import { WorkflowEditActionHttpRequest } from '@/workflow/workflow-steps/workflow-actions/http-request-action/components/WorkflowEditActionHttpRequest'; import { WorkflowEditActionIterator } from '@/workflow/workflow-steps/workflow-actions/iterator-action/components/WorkflowEditActionIterator'; @@ -230,6 +231,17 @@ export const WorkflowRunStepNodeDetail = ({ /> ); } + case 'IF_ELSE': { + return ( + + ); + } case 'ITERATOR': { return ( ); } + case 'IF_ELSE': { + return ( + + ); + } case 'ITERATOR': { return ( { const { readonly } = useContext(WorkflowStepFilterContext); + const stepFilterGroups = useRecoilComponentValue( + currentStepFilterGroupsComponentState, + ); + + const rootStepFilterGroup = stepFilterGroups?.find( + (filterGroup) => !isDefined(filterGroup.parentStepFilterGroupId), + ); + + const { childStepFilters, childStepFilterGroups } = + useChildStepFiltersAndChildStepFilterGroups({ + stepFilterGroupId: rootStepFilterGroup?.id ?? '', + }); + + const isLastFilterInIfBranch = + isIfBranch && + isDefined(rootStepFilterGroup) && + stepFilter.stepFilterGroupId === rootStepFilterGroup.id && + childStepFilters.length === 1 && + childStepFilterGroups.length === 0; + + const shouldShowDropdown = !readonly && !isLastFilterInIfBranch; + return ( - {!readonly && ( + {shouldShowDropdown && ( )} diff --git a/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/filter-action/components/WorkflowStepFilterFieldSelect.tsx b/packages/twenty-front/src/modules/workflow/workflow-steps/filters/components/WorkflowStepFilterFieldSelect.tsx similarity index 96% rename from packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/filter-action/components/WorkflowStepFilterFieldSelect.tsx rename to packages/twenty-front/src/modules/workflow/workflow-steps/filters/components/WorkflowStepFilterFieldSelect.tsx index 7ef4aa4fbe..4a83956f1f 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/filter-action/components/WorkflowStepFilterFieldSelect.tsx +++ b/packages/twenty-front/src/modules/workflow/workflow-steps/filters/components/WorkflowStepFilterFieldSelect.tsx @@ -2,8 +2,8 @@ import { useFieldMetadataItemById } from '@/object-metadata/hooks/useFieldMetada import { SelectControl } from '@/ui/input/components/SelectControl'; import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown'; import { useCloseDropdown } from '@/ui/layout/dropdown/hooks/useCloseDropdown'; -import { WorkflowDropdownStepOutputItems } from '@/workflow/workflow-steps/workflow-actions/filter-action/components/WorkflowDropdownStepOutputItems'; -import { WorkflowStepFilterContext } from '@/workflow/workflow-steps/workflow-actions/filter-action/states/context/WorkflowStepFilterContext'; +import { WorkflowDropdownStepOutputItems } from '@/workflow/workflow-steps/components/WorkflowDropdownStepOutputItems'; +import { WorkflowStepFilterContext } from '@/workflow/workflow-steps/filters/states/context/WorkflowStepFilterContext'; import { WorkflowVariablesDropdownSteps } from '@/workflow/workflow-variables/components/WorkflowVariablesDropdownSteps'; import { useAvailableVariablesInWorkflowStep } from '@/workflow/workflow-variables/hooks/useAvailableVariablesInWorkflowStep'; import { useSearchVariable } from '@/workflow/workflow-variables/hooks/useSearchVariable'; diff --git a/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/filter-action/components/WorkflowStepFilterGroupChildren.tsx b/packages/twenty-front/src/modules/workflow/workflow-steps/filters/components/WorkflowStepFilterGroupChildren.tsx similarity index 84% rename from packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/filter-action/components/WorkflowStepFilterGroupChildren.tsx rename to packages/twenty-front/src/modules/workflow/workflow-steps/filters/components/WorkflowStepFilterGroupChildren.tsx index d504c9276e..3ed2dbd832 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/filter-action/components/WorkflowStepFilterGroupChildren.tsx +++ b/packages/twenty-front/src/modules/workflow/workflow-steps/filters/components/WorkflowStepFilterGroupChildren.tsx @@ -1,7 +1,7 @@ -import { WorkflowStepFilterAddFilterRuleSelect } from '@/workflow/workflow-steps/workflow-actions/filter-action/components/WorkflowStepFilterAddFilterRuleSelect'; -import { WorkflowStepFilterColumn } from '@/workflow/workflow-steps/workflow-actions/filter-action/components/WorkflowStepFilterColumn'; -import { useChildStepFiltersAndChildStepFilterGroups } from '@/workflow/workflow-steps/workflow-actions/filter-action/hooks/useChildStepFiltersAndChildStepFilterGroups'; -import { WorkflowStepFilterContext } from '@/workflow/workflow-steps/workflow-actions/filter-action/states/context/WorkflowStepFilterContext'; +import { WorkflowStepFilterAddFilterRuleSelect } from '@/workflow/workflow-steps/filters/components/WorkflowStepFilterAddFilterRuleSelect'; +import { WorkflowStepFilterColumn } from '@/workflow/workflow-steps/filters/components/WorkflowStepFilterColumn'; +import { useChildStepFiltersAndChildStepFilterGroups } from '@/workflow/workflow-steps/filters/hooks/useChildStepFiltersAndChildStepFilterGroups'; +import { WorkflowStepFilterContext } from '@/workflow/workflow-steps/filters/states/context/WorkflowStepFilterContext'; import styled from '@emotion/styled'; import { useContext } from 'react'; import { isDefined } from 'twenty-shared/utils'; diff --git a/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/filter-action/components/WorkflowStepFilterGroupColumn.tsx b/packages/twenty-front/src/modules/workflow/workflow-steps/filters/components/WorkflowStepFilterGroupColumn.tsx similarity index 81% rename from packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/filter-action/components/WorkflowStepFilterGroupColumn.tsx rename to packages/twenty-front/src/modules/workflow/workflow-steps/filters/components/WorkflowStepFilterGroupColumn.tsx index d4072478ac..3ed98a276f 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/filter-action/components/WorkflowStepFilterGroupColumn.tsx +++ b/packages/twenty-front/src/modules/workflow/workflow-steps/filters/components/WorkflowStepFilterGroupColumn.tsx @@ -1,8 +1,8 @@ import { AdvancedFilterCommandMenuColumn } from '@/object-record/advanced-filter/command-menu/components/AdvancedFilterCommandMenuColumn'; -import { WorkflowStepFilterGroupChildren } from '@/workflow/workflow-steps/workflow-actions/filter-action/components/WorkflowStepFilterGroupChildren'; -import { WorkflowStepFilterGroupOptionsDropdown } from '@/workflow/workflow-steps/workflow-actions/filter-action/components/WorkflowStepFilterGroupOptionsDropdown'; -import { WorkflowStepFilterLogicalOperatorCell } from '@/workflow/workflow-steps/workflow-actions/filter-action/components/WorkflowStepFilterLogicalOperatorCell'; -import { WorkflowStepFilterContext } from '@/workflow/workflow-steps/workflow-actions/filter-action/states/context/WorkflowStepFilterContext'; +import { WorkflowStepFilterGroupChildren } from '@/workflow/workflow-steps/filters/components/WorkflowStepFilterGroupChildren'; +import { WorkflowStepFilterGroupOptionsDropdown } from '@/workflow/workflow-steps/filters/components/WorkflowStepFilterGroupOptionsDropdown'; +import { WorkflowStepFilterLogicalOperatorCell } from '@/workflow/workflow-steps/filters/components/WorkflowStepFilterLogicalOperatorCell'; +import { WorkflowStepFilterContext } from '@/workflow/workflow-steps/filters/states/context/WorkflowStepFilterContext'; import styled from '@emotion/styled'; import { useContext } from 'react'; import { type StepFilterGroup } from 'twenty-shared/types'; diff --git a/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/filter-action/components/WorkflowStepFilterGroupOptionsDropdown.tsx b/packages/twenty-front/src/modules/workflow/workflow-steps/filters/components/WorkflowStepFilterGroupOptionsDropdown.tsx similarity index 92% rename from packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/filter-action/components/WorkflowStepFilterGroupOptionsDropdown.tsx rename to packages/twenty-front/src/modules/workflow/workflow-steps/filters/components/WorkflowStepFilterGroupOptionsDropdown.tsx index 5ae2bba7aa..6daf32b5b5 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/filter-action/components/WorkflowStepFilterGroupOptionsDropdown.tsx +++ b/packages/twenty-front/src/modules/workflow/workflow-steps/filters/components/WorkflowStepFilterGroupOptionsDropdown.tsx @@ -2,8 +2,8 @@ import { DEFAULT_ADVANCED_FILTER_DROPDOWN_OFFSET } from '@/object-record/advance import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown'; import { DropdownContent } from '@/ui/layout/dropdown/components/DropdownContent'; import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer'; -import { useRemoveStepFilterGroup } from '@/workflow/workflow-steps/workflow-actions/filter-action/hooks/useRemoveStepFilterGroup'; -import { WorkflowStepFilterContext } from '@/workflow/workflow-steps/workflow-actions/filter-action/states/context/WorkflowStepFilterContext'; +import { useRemoveStepFilterGroup } from '@/workflow/workflow-steps/filters/hooks/useRemoveStepFilterGroup'; +import { WorkflowStepFilterContext } from '@/workflow/workflow-steps/filters/states/context/WorkflowStepFilterContext'; import { useContext } from 'react'; import { t } from '@lingui/core/macro'; import { IconDotsVertical, IconTrash } from 'twenty-ui/display'; diff --git a/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/filter-action/components/WorkflowStepFilterLogicalOperatorCell.tsx b/packages/twenty-front/src/modules/workflow/workflow-steps/filters/components/WorkflowStepFilterLogicalOperatorCell.tsx similarity index 91% rename from packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/filter-action/components/WorkflowStepFilterLogicalOperatorCell.tsx rename to packages/twenty-front/src/modules/workflow/workflow-steps/filters/components/WorkflowStepFilterLogicalOperatorCell.tsx index a50e237b95..763c67bc30 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/filter-action/components/WorkflowStepFilterLogicalOperatorCell.tsx +++ b/packages/twenty-front/src/modules/workflow/workflow-steps/filters/components/WorkflowStepFilterLogicalOperatorCell.tsx @@ -1,11 +1,11 @@ import { DEFAULT_ADVANCED_FILTER_DROPDOWN_OFFSET } from '@/object-record/advanced-filter/constants/DefaultAdvancedFilterDropdownOffset'; import { Select } from '@/ui/input/components/Select'; import { GenericDropdownContentWidth } from '@/ui/layout/dropdown/constants/GenericDropdownContentWidth'; -import { useUpsertStepFilterSettings } from '@/workflow/workflow-steps/workflow-actions/filter-action/hooks/useUpsertStepFilterSettings'; -import { WorkflowStepFilterContext } from '@/workflow/workflow-steps/workflow-actions/filter-action/states/context/WorkflowStepFilterContext'; +import { useUpsertStepFilterSettings } from '@/workflow/workflow-steps/filters/hooks/useUpsertStepFilterSettings'; +import { WorkflowStepFilterContext } from '@/workflow/workflow-steps/filters/states/context/WorkflowStepFilterContext'; -import { useLingui } from '@lingui/react/macro'; import styled from '@emotion/styled'; +import { useLingui } from '@lingui/react/macro'; import { useContext, useMemo } from 'react'; import { StepLogicalOperator, type StepFilterGroup } from 'twenty-shared/types'; import { capitalize } from 'twenty-shared/utils'; @@ -27,14 +27,17 @@ const StyledContainer = styled.div` type WorkflowStepFilterLogicalOperatorCellProps = { index: number; stepFilterGroup: StepFilterGroup; + firstFilterLabel?: string; }; export const WorkflowStepFilterLogicalOperatorCell = ({ index, stepFilterGroup, + firstFilterLabel, }: WorkflowStepFilterLogicalOperatorCellProps) => { const { readonly } = useContext(WorkflowStepFilterContext); const { t } = useLingui(); + const defaultFirstFilterLabel = t`Where`; const { upsertStepFilterSettings } = useUpsertStepFilterSettings(); @@ -66,7 +69,7 @@ export const WorkflowStepFilterLogicalOperatorCell = ({ return ( {index === 0 ? ( - {t`Where`} + {firstFilterLabel ?? defaultFirstFilterLabel} ) : index === 1 ? ( readonly ? (