From 3fbc257c06bc60db3e2d9f27a198bdc8c3ef0723 Mon Sep 17 00:00:00 2001 From: Thomas Trompette Date: Thu, 18 Sep 2025 15:07:23 +0200 Subject: [PATCH] Make filters a regular step (#14586) - Remove special design logic for filters and edge with filters - Remove backend logic to delete filters along with parent steps - Add filters to the node type picker https://github.com/user-attachments/assets/5b6ceda4-d86f-48a3-8c93-37a885398a8d --- .../CommandMenuWorkflowSelectAction.tsx | 24 +- .../WorkflowDiagramCanvasEditable.tsx | 4 +- .../WorkflowDiagramCanvasReadonly.tsx | 4 +- .../components/WorkflowRunDiagramCanvas.tsx | 4 +- .../useOpenWorkflowEditFilterInCommandMenu.ts | 65 --- .../useOpenWorkflowRunFilterInCommandMenu.ts | 78 --- .../useOpenWorkflowViewFilterInCommandMenu.ts | 72 --- .../workflow-diagram/types/WorkflowDiagram.ts | 24 +- .../generateWorkflowRunDiagram.test.ts | 28 +- .../getWorkflowVersionDiagram.test.ts | 2 +- .../transformFilterNodesAsEdges.test.ts | 511 ------------------ .../utils/generateWorkflowRunDiagram.ts | 11 +- .../utils/getEdgeTypeBetweenTwoNodes.ts | 4 +- .../utils/getFilterEdgeType.ts | 20 - .../utils/getWorkflowDiagramColors.ts | 4 +- .../utils/getWorkflowVersionDiagram.ts | 9 +- .../utils/transformFilterNodesAsEdges.ts | 95 ---- .../WorkflowDiagramDefaultEdgeEditable.tsx | 45 +- .../WorkflowDiagramFilterEdgeEditable.tsx | 306 ----------- .../WorkflowDiagramFilterEdgeReadonly.tsx | 143 ----- .../WorkflowDiagramFilterEdgeRun.tsx | 141 ----- ...kflowVisualizerEdgeDefaultConfiguration.ts | 4 +- .../components/WorkflowRunDiagramStepNode.tsx | 3 +- .../{OtherActions.ts => FlowActions.ts} | 9 +- .../WorkflowDropdownStepOutputItems.tsx | 13 +- .../components/WorkflowStepFilterCounter.tsx | 37 -- .../WorkflowStepFilterCounter.stories.tsx | 28 - .../WorkflowVariablesDropdownStepItems.tsx | 3 +- ...low-version-edge.workspace-service.spec.ts | 190 ------- ...workflow-version-edge.workspace-service.ts | 151 +----- .../utils/__tests__/remove-step.spec.ts | 96 +--- .../utils/remove-step.ts | 144 +---- 32 files changed, 88 insertions(+), 2184 deletions(-) delete mode 100644 packages/twenty-front/src/modules/workflow/workflow-diagram/hooks/useOpenWorkflowEditFilterInCommandMenu.ts delete mode 100644 packages/twenty-front/src/modules/workflow/workflow-diagram/hooks/useOpenWorkflowRunFilterInCommandMenu.ts delete mode 100644 packages/twenty-front/src/modules/workflow/workflow-diagram/hooks/useOpenWorkflowViewFilterInCommandMenu.ts delete mode 100644 packages/twenty-front/src/modules/workflow/workflow-diagram/utils/__tests__/transformFilterNodesAsEdges.test.ts delete mode 100644 packages/twenty-front/src/modules/workflow/workflow-diagram/utils/getFilterEdgeType.ts delete mode 100644 packages/twenty-front/src/modules/workflow/workflow-diagram/utils/transformFilterNodesAsEdges.ts delete mode 100644 packages/twenty-front/src/modules/workflow/workflow-diagram/workflow-edges/components/WorkflowDiagramFilterEdgeEditable.tsx delete mode 100644 packages/twenty-front/src/modules/workflow/workflow-diagram/workflow-edges/components/WorkflowDiagramFilterEdgeReadonly.tsx delete mode 100644 packages/twenty-front/src/modules/workflow/workflow-diagram/workflow-edges/components/WorkflowDiagramFilterEdgeRun.tsx rename packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/constants/{OtherActions.ts => FlowActions.ts} (53%) delete mode 100644 packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/filter-action/components/WorkflowStepFilterCounter.tsx delete mode 100644 packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/filter-action/components/__stories__/WorkflowStepFilterCounter.stories.tsx 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 88ea2896ce7..bc63b9cccca 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 @@ -4,8 +4,8 @@ import { RightDrawerStepListContainer } from '@/workflow/workflow-steps/componen import { RightDrawerWorkflowSelectStepTitle } from '@/workflow/workflow-steps/components/RightDrawerWorkflowSelectStepTitle'; import { AI_ACTIONS } from '@/workflow/workflow-steps/workflow-actions/constants/AiActions'; import { CORE_ACTIONS } from '@/workflow/workflow-steps/workflow-actions/constants/CoreActions'; +import { FLOW_ACTIONS } from '@/workflow/workflow-steps/workflow-actions/constants/FlowActions'; import { HUMAN_INPUT_ACTIONS } from '@/workflow/workflow-steps/workflow-actions/constants/HumanInputActions'; -import { OTHER_ACTIONS } from '@/workflow/workflow-steps/workflow-actions/constants/OtherActions'; import { RECORD_ACTIONS } from '@/workflow/workflow-steps/workflow-actions/constants/RecordActions'; import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled'; import { useLingui } from '@lingui/react/macro'; @@ -45,6 +45,16 @@ export const CommandMenuWorkflowSelectAction = ({ )} + + {t`Flow`} + + action.type !== 'ITERATOR' || isIteratorEnabled, + )} + onClick={onActionSelected} + /> + {t`Core`} @@ -60,18 +70,6 @@ export const CommandMenuWorkflowSelectAction = ({ actions={HUMAN_INPUT_ACTIONS} onClick={onActionSelected} /> - - {isIteratorEnabled && ( - <> - - {t`Others`} - - - - )} ); }; 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 a241edb86aa..9baba86534a 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 @@ -14,7 +14,6 @@ import { import { getWorkflowVersionStatusTagProps } from '@/workflow/workflow-diagram/utils/getWorkflowVersionStatusTagProps'; 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 { WorkflowDiagramStepNodeEditable } from '@/workflow/workflow-diagram/workflow-nodes/components/WorkflowDiagramStepNodeEditable'; import { useCreateEdge } from '@/workflow/workflow-steps/hooks/useCreateEdge'; @@ -128,8 +127,7 @@ export const WorkflowDiagramCanvasEditable = () => { }} edgeTypes={{ blank: WorkflowDiagramBlankEdge, - 'empty-filter--editable': WorkflowDiagramDefaultEdgeEditable, - 'filter--editable': WorkflowDiagramFilterEdgeEditable, + editable: WorkflowDiagramDefaultEdgeEditable, }} tagContainerTestId="workflow-visualizer-status" tagColor={tagProps.color} diff --git a/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowDiagramCanvasReadonly.tsx b/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowDiagramCanvasReadonly.tsx index 682179c74b3..36dc41be4ca 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowDiagramCanvasReadonly.tsx +++ b/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowDiagramCanvasReadonly.tsx @@ -4,7 +4,6 @@ import { WorkflowDiagramDefaultEdgeReadonly } from '@/workflow/workflow-diagram/ import { getWorkflowVersionStatusTagProps } from '@/workflow/workflow-diagram/utils/getWorkflowVersionStatusTagProps'; import { WorkflowDiagramEmptyTriggerReadonly } from '@/workflow/workflow-diagram/workflow-nodes/components/WorkflowDiagramEmptyTriggerReadonly'; -import { WorkflowDiagramFilterEdgeReadonly } from '@/workflow/workflow-diagram/workflow-edges/components/WorkflowDiagramFilterEdgeReadonly'; import { WorkflowDiagramStepNodeReadonly } from '@/workflow/workflow-diagram/workflow-nodes/components/WorkflowDiagramStepNodeReadonly'; import { ReactFlowProvider } from '@xyflow/react'; @@ -25,8 +24,7 @@ export const WorkflowDiagramCanvasReadonly = ({ 'empty-trigger': WorkflowDiagramEmptyTriggerReadonly, }} edgeTypes={{ - 'empty-filter--readonly': WorkflowDiagramDefaultEdgeReadonly, - 'filter--readonly': WorkflowDiagramFilterEdgeReadonly, + readonly: WorkflowDiagramDefaultEdgeReadonly, }} tagContainerTestId="workflow-visualizer-status" tagColor={tagProps.color} diff --git a/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowRunDiagramCanvas.tsx b/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowRunDiagramCanvas.tsx index 86cd7978005..b6073244ad6 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowRunDiagramCanvas.tsx +++ b/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowRunDiagramCanvas.tsx @@ -1,7 +1,6 @@ import { type WorkflowRunStatus } from '@/workflow/types/Workflow'; import { WorkflowDiagramCanvasBase } from '@/workflow/workflow-diagram/components/WorkflowDiagramCanvasBase'; import { WorkflowDiagramDefaultEdgeRun } from '@/workflow/workflow-diagram/workflow-edges/components/WorkflowDiagramDefaultEdgeRun'; -import { WorkflowDiagramFilterEdgeRun } from '@/workflow/workflow-diagram/workflow-edges/components/WorkflowDiagramFilterEdgeRun'; import { getWorkflowRunStatusTagProps } from '@/workflow/workflow-diagram/utils/getWorkflowRunStatusTagProps'; import { WorkflowRunDiagramStepNode } from '@/workflow/workflow-diagram/workflow-nodes/components/WorkflowRunDiagramStepNode'; @@ -23,8 +22,7 @@ export const WorkflowRunDiagramCanvas = ({ default: WorkflowRunDiagramStepNode, }} edgeTypes={{ - 'empty-filter--run': WorkflowDiagramDefaultEdgeRun, - 'filter--run': WorkflowDiagramFilterEdgeRun, + run: WorkflowDiagramDefaultEdgeRun, }} tagContainerTestId="workflow-run-status" tagColor={tagProps.color} diff --git a/packages/twenty-front/src/modules/workflow/workflow-diagram/hooks/useOpenWorkflowEditFilterInCommandMenu.ts b/packages/twenty-front/src/modules/workflow/workflow-diagram/hooks/useOpenWorkflowEditFilterInCommandMenu.ts deleted file mode 100644 index e27b4ecf5ee..00000000000 --- a/packages/twenty-front/src/modules/workflow/workflow-diagram/hooks/useOpenWorkflowEditFilterInCommandMenu.ts +++ /dev/null @@ -1,65 +0,0 @@ -import { useWorkflowCommandMenu } from '@/command-menu/hooks/useWorkflowCommandMenu'; -import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue'; -import { useSetRecoilComponentState } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentState'; -import { workflowVisualizerWorkflowIdComponentState } from '@/workflow/states/workflowVisualizerWorkflowIdComponentState'; -import { workflowDiagramComponentState } from '@/workflow/workflow-diagram/states/workflowDiagramComponentState'; -import { workflowSelectedNodeComponentState } from '@/workflow/workflow-diagram/states/workflowSelectedNodeComponentState'; -import { getActionIcon } from '@/workflow/workflow-steps/workflow-actions/utils/getActionIcon'; -import { isDefined } from 'twenty-shared/utils'; -import { useIcons } from 'twenty-ui/display'; - -export const useOpenWorkflowEditFilterInCommandMenu = () => { - const { getIcon } = useIcons(); - - const workflowVisualizerWorkflowId = useRecoilComponentValue( - workflowVisualizerWorkflowIdComponentState, - ); - const { openWorkflowEditStepInCommandMenu } = useWorkflowCommandMenu(); - - const setWorkflowSelectedNode = useSetRecoilComponentState( - workflowSelectedNodeComponentState, - ); - const setWorkflowDiagram = useSetRecoilComponentState( - workflowDiagramComponentState, - ); - - const openWorkflowEditFilterInCommandMenu = ({ - stepId, - stepName, - }: { - stepId: string; - stepName: string; - }) => { - if (!isDefined(workflowVisualizerWorkflowId)) { - throw new Error( - 'Workflow ID must be configured for the edge when opening a filter in command menu', - ); - } - - setWorkflowSelectedNode(stepId); - - setWorkflowDiagram((diagram) => { - if (!isDefined(diagram)) { - throw new Error('Workflow diagram must be defined'); - } - - return { - ...diagram, - nodes: diagram.nodes.map((node) => ({ - ...node, - selected: node.id === stepId, - })), - }; - }); - - openWorkflowEditStepInCommandMenu( - workflowVisualizerWorkflowId, - stepName, - getIcon(getActionIcon('FILTER')), - ); - }; - - return { - openWorkflowEditFilterInCommandMenu, - }; -}; diff --git a/packages/twenty-front/src/modules/workflow/workflow-diagram/hooks/useOpenWorkflowRunFilterInCommandMenu.ts b/packages/twenty-front/src/modules/workflow/workflow-diagram/hooks/useOpenWorkflowRunFilterInCommandMenu.ts deleted file mode 100644 index 3f2b0b76629..00000000000 --- a/packages/twenty-front/src/modules/workflow/workflow-diagram/hooks/useOpenWorkflowRunFilterInCommandMenu.ts +++ /dev/null @@ -1,78 +0,0 @@ -import { useWorkflowCommandMenu } from '@/command-menu/hooks/useWorkflowCommandMenu'; -import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue'; -import { useSetRecoilComponentState } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentState'; -import { workflowVisualizerWorkflowIdComponentState } from '@/workflow/states/workflowVisualizerWorkflowIdComponentState'; -import { workflowVisualizerWorkflowRunIdComponentState } from '@/workflow/states/workflowVisualizerWorkflowRunIdComponentState'; -import { workflowDiagramComponentState } from '@/workflow/workflow-diagram/states/workflowDiagramComponentState'; -import { workflowSelectedNodeComponentState } from '@/workflow/workflow-diagram/states/workflowSelectedNodeComponentState'; -import { getActionIcon } from '@/workflow/workflow-steps/workflow-actions/utils/getActionIcon'; -import { isDefined } from 'twenty-shared/utils'; -import { type StepStatus } from 'twenty-shared/workflow'; -import { useIcons } from 'twenty-ui/display'; - -export const useOpenWorkflowRunFilterInCommandMenu = () => { - const { getIcon } = useIcons(); - - const workflowVisualizerWorkflowId = useRecoilComponentValue( - workflowVisualizerWorkflowIdComponentState, - ); - const workflowVisualizerWorkflowRunId = useRecoilComponentValue( - workflowVisualizerWorkflowRunIdComponentState, - ); - - const setWorkflowSelectedNode = useSetRecoilComponentState( - workflowSelectedNodeComponentState, - ); - const setWorkflowDiagram = useSetRecoilComponentState( - workflowDiagramComponentState, - ); - - const { openWorkflowRunViewStepInCommandMenu } = useWorkflowCommandMenu(); - - const openWorkflowRunFilterInCommandMenu = ({ - stepId, - stepName, - stepExecutionStatus, - }: { - stepId: string; - stepName: string; - stepExecutionStatus: StepStatus; - }) => { - if (!isDefined(workflowVisualizerWorkflowId)) { - throw new Error('Workflow ID is required'); - } - - if (!isDefined(workflowVisualizerWorkflowRunId)) { - throw new Error('Workflow run ID is required'); - } - - setWorkflowSelectedNode(stepId); - - setWorkflowDiagram((diagram) => { - if (!isDefined(diagram)) { - throw new Error('Workflow diagram must be defined'); - } - - return { - ...diagram, - nodes: diagram.nodes.map((node) => ({ - ...node, - selected: false, - })), - }; - }); - - openWorkflowRunViewStepInCommandMenu({ - workflowId: workflowVisualizerWorkflowId, - workflowRunId: workflowVisualizerWorkflowRunId, - title: stepName, - icon: getIcon(getActionIcon('FILTER')), - workflowSelectedNode: stepId, - stepExecutionStatus, - }); - }; - - return { - openWorkflowRunFilterInCommandMenu, - }; -}; diff --git a/packages/twenty-front/src/modules/workflow/workflow-diagram/hooks/useOpenWorkflowViewFilterInCommandMenu.ts b/packages/twenty-front/src/modules/workflow/workflow-diagram/hooks/useOpenWorkflowViewFilterInCommandMenu.ts deleted file mode 100644 index 1decb7c1a31..00000000000 --- a/packages/twenty-front/src/modules/workflow/workflow-diagram/hooks/useOpenWorkflowViewFilterInCommandMenu.ts +++ /dev/null @@ -1,72 +0,0 @@ -import { useWorkflowCommandMenu } from '@/command-menu/hooks/useWorkflowCommandMenu'; -import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue'; -import { useSetRecoilComponentState } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentState'; -import { workflowVisualizerWorkflowIdComponentState } from '@/workflow/states/workflowVisualizerWorkflowIdComponentState'; -import { workflowVisualizerWorkflowVersionIdComponentState } from '@/workflow/states/workflowVisualizerWorkflowVersionIdComponentState'; -import { workflowDiagramComponentState } from '@/workflow/workflow-diagram/states/workflowDiagramComponentState'; -import { workflowSelectedNodeComponentState } from '@/workflow/workflow-diagram/states/workflowSelectedNodeComponentState'; -import { getActionIcon } from '@/workflow/workflow-steps/workflow-actions/utils/getActionIcon'; -import { isDefined } from 'twenty-shared/utils'; -import { useIcons } from 'twenty-ui/display'; - -export const useOpenWorkflowViewFilterInCommandMenu = () => { - const { getIcon } = useIcons(); - - const workflowVisualizerWorkflowId = useRecoilComponentValue( - workflowVisualizerWorkflowIdComponentState, - ); - const workflowVisualizerWorkflowVersionId = useRecoilComponentValue( - workflowVisualizerWorkflowVersionIdComponentState, - ); - const { openWorkflowViewStepInCommandMenu } = useWorkflowCommandMenu(); - - const setWorkflowSelectedNode = useSetRecoilComponentState( - workflowSelectedNodeComponentState, - ); - const setWorkflowDiagram = useSetRecoilComponentState( - workflowDiagramComponentState, - ); - - const openWorkflowViewFilterInCommandMenu = ({ - stepId, - stepName, - }: { - stepId: string; - stepName: string; - }) => { - if (!workflowVisualizerWorkflowId) { - throw new Error('Workflow ID is required'); - } - - if (!workflowVisualizerWorkflowVersionId) { - throw new Error('Workflow version ID is required'); - } - - setWorkflowSelectedNode(stepId); - - setWorkflowDiagram((diagram) => { - if (!isDefined(diagram)) { - throw new Error('Workflow diagram must be defined'); - } - - return { - ...diagram, - nodes: diagram.nodes.map((node) => ({ - ...node, - selected: false, - })), - }; - }); - - openWorkflowViewStepInCommandMenu({ - workflowId: workflowVisualizerWorkflowId, - workflowVersionId: workflowVisualizerWorkflowVersionId, - title: stepName, - icon: getIcon(getActionIcon('FILTER')), - }); - }; - - return { - openWorkflowViewFilterInCommandMenu, - }; -}; 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 25eb8618e11..73ec0d284e4 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,7 +3,6 @@ import { type WorkflowRunStepStatus, type WorkflowTriggerType, } from '@/workflow/types/Workflow'; -import { type FilterSettings } from '@/workflow/workflow-steps/workflow-actions/filter-action/components/WorkflowEditActionFilter'; import { type MessageDescriptor } from '@lingui/core'; import { type Connection, @@ -105,33 +104,14 @@ export type WorkflowDiagramEdgeLabelOptions = { label: MessageDescriptor; }; -export type WorkflowDiagramFilterEdgeData = { - edgeType: 'filter'; - stepId: string; - filterSettings: FilterSettings; - name: string; - runStatus?: WorkflowRunStepStatus; - edgeExecutionStatus?: StepStatus; - labelOptions?: WorkflowDiagramEdgeLabelOptions; -}; - export type WorkflowDiagramDefaultEdgeData = { edgeType: 'default'; edgeExecutionStatus?: StepStatus; labelOptions?: WorkflowDiagramEdgeLabelOptions; }; -export type WorkflowDiagramEdgeData = - | WorkflowDiagramFilterEdgeData - | WorkflowDiagramDefaultEdgeData; +export type WorkflowDiagramEdgeData = WorkflowDiagramDefaultEdgeData; export type WorkflowDiagramNodeType = 'default' | 'empty-trigger'; -export type WorkflowDiagramEdgeType = - | 'blank' - | 'empty-filter--editable' - | 'empty-filter--readonly' - | 'empty-filter--run' - | 'filter--editable' - | 'filter--readonly' - | 'filter--run'; +export type WorkflowDiagramEdgeType = 'blank' | 'editable' | 'readonly' | 'run'; diff --git a/packages/twenty-front/src/modules/workflow/workflow-diagram/utils/__tests__/generateWorkflowRunDiagram.test.ts b/packages/twenty-front/src/modules/workflow/workflow-diagram/utils/__tests__/generateWorkflowRunDiagram.test.ts index a2ea74ad897..3cb6cc27d00 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-diagram/utils/__tests__/generateWorkflowRunDiagram.test.ts +++ b/packages/twenty-front/src/modules/workflow/workflow-diagram/utils/__tests__/generateWorkflowRunDiagram.test.ts @@ -124,7 +124,7 @@ describe('generateWorkflowRunDiagram', () => { "sourceHandle": "default", "target": "step1", "targetHandle": "default", - "type": "empty-filter--run", + "type": "run", "zIndex": -2, }, { @@ -141,7 +141,7 @@ describe('generateWorkflowRunDiagram', () => { "sourceHandle": "default", "target": "step2", "targetHandle": "default", - "type": "empty-filter--run", + "type": "run", "zIndex": -2, }, { @@ -158,7 +158,7 @@ describe('generateWorkflowRunDiagram', () => { "sourceHandle": "default", "target": "step3", "targetHandle": "default", - "type": "empty-filter--run", + "type": "run", "zIndex": -2, }, ], @@ -361,7 +361,7 @@ describe('generateWorkflowRunDiagram', () => { "sourceHandle": "default", "target": "step1", "targetHandle": "default", - "type": "empty-filter--run", + "type": "run", "zIndex": -2, }, { @@ -378,7 +378,7 @@ describe('generateWorkflowRunDiagram', () => { "sourceHandle": "default", "target": "step2", "targetHandle": "default", - "type": "empty-filter--run", + "type": "run", "zIndex": -2, }, { @@ -395,7 +395,7 @@ describe('generateWorkflowRunDiagram', () => { "sourceHandle": "default", "target": "step3", "targetHandle": "default", - "type": "empty-filter--run", + "type": "run", "zIndex": -2, }, ], @@ -598,7 +598,7 @@ describe('generateWorkflowRunDiagram', () => { "sourceHandle": "default", "target": "step1", "targetHandle": "default", - "type": "empty-filter--run", + "type": "run", "zIndex": -2, }, { @@ -615,7 +615,7 @@ describe('generateWorkflowRunDiagram', () => { "sourceHandle": "default", "target": "step2", "targetHandle": "default", - "type": "empty-filter--run", + "type": "run", "zIndex": -2, }, { @@ -632,7 +632,7 @@ describe('generateWorkflowRunDiagram', () => { "sourceHandle": "default", "target": "step3", "targetHandle": "default", - "type": "empty-filter--run", + "type": "run", "zIndex": -2, }, ], @@ -854,7 +854,7 @@ describe('generateWorkflowRunDiagram', () => { "sourceHandle": "default", "target": "step1", "targetHandle": "default", - "type": "empty-filter--run", + "type": "run", "zIndex": -2, }, { @@ -871,7 +871,7 @@ describe('generateWorkflowRunDiagram', () => { "sourceHandle": "default", "target": "step2", "targetHandle": "default", - "type": "empty-filter--run", + "type": "run", "zIndex": -2, }, { @@ -888,7 +888,7 @@ describe('generateWorkflowRunDiagram', () => { "sourceHandle": "default", "target": "step3", "targetHandle": "default", - "type": "empty-filter--run", + "type": "run", "zIndex": -2, }, { @@ -905,7 +905,7 @@ describe('generateWorkflowRunDiagram', () => { "sourceHandle": "default", "target": "step4", "targetHandle": "default", - "type": "empty-filter--run", + "type": "run", "zIndex": -2, }, ], @@ -1093,7 +1093,7 @@ describe('generateWorkflowRunDiagram', () => { "sourceHandle": "default", "target": "step1", "targetHandle": "default", - "type": "empty-filter--run", + "type": "run", "zIndex": -2, }, ], 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 61b7175090b..217d23cbf12 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 @@ -164,7 +164,7 @@ describe('getWorkflowVersionDiagram', () => { "sourceHandle": "default", "target": "step-1", "targetHandle": "default", - "type": "empty-filter--editable", + "type": "editable", "zIndex": -2, }, ], 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 deleted file mode 100644 index 112bc58533a..00000000000 --- a/packages/twenty-front/src/modules/workflow/workflow-diagram/utils/__tests__/transformFilterNodesAsEdges.test.ts +++ /dev/null @@ -1,511 +0,0 @@ -import { type WorkflowDiagram } from '@/workflow/workflow-diagram/types/WorkflowDiagram'; -import { transformFilterNodesAsEdges } from '../transformFilterNodesAsEdges'; - -describe('transformFilterNodesAsEdges', () => { - it('should return the original diagram when there are no filter nodes', () => { - const diagram: WorkflowDiagram = { - nodes: [ - { - id: 'A', - data: { - nodeType: 'action', - actionType: 'CODE', - name: 'Step A', - hasNextStepIds: false, - position: { x: 0, y: 0 }, - stepId: 'A', - }, - position: { x: 0, y: 0 }, - }, - { - id: 'C', - data: { - nodeType: 'action', - actionType: 'SEND_EMAIL', - name: 'Step C', - hasNextStepIds: false, - position: { x: 0, y: 0 }, - stepId: 'C', - }, - position: { x: 0, y: 300 }, - }, - ], - edges: [ - { - id: 'A-C', - source: 'A', - sourceHandle: 'default', - target: 'C', - targetHandle: 'default', - data: { - edgeType: 'default', - }, - }, - ], - }; - - const result = transformFilterNodesAsEdges({ - nodes: diagram.nodes, - edges: diagram.edges, - workflowContext: 'workflow', - }); - - expect(result.nodes).toEqual(diagram.nodes); - expect(result.edges).toEqual(diagram.edges); - }); - - it('should transform A->B->C where B is a FILTER step', () => { - const diagram: WorkflowDiagram = { - nodes: [ - { - id: 'A', - data: { - nodeType: 'action', - actionType: 'CODE', - name: 'Step A', - hasNextStepIds: true, - position: { x: 0, y: 0 }, - stepId: 'A', - }, - position: { x: 0, y: 0 }, - }, - { - id: 'B', - data: { - nodeType: 'action', - actionType: 'FILTER', - name: 'Filter B', - hasNextStepIds: true, - position: { x: 0, y: 150 }, - stepId: 'B', - }, - position: { x: 0, y: 150 }, - }, - { - id: 'C', - data: { - nodeType: 'action', - actionType: 'SEND_EMAIL', - name: 'Step C', - hasNextStepIds: false, - position: { x: 0, y: 300 }, - stepId: 'C', - }, - position: { x: 0, y: 300 }, - }, - ], - edges: [ - { - id: 'A-B', - source: 'A', - sourceHandle: 'default', - target: 'B', - targetHandle: 'default', - data: { edgeType: 'default' }, - }, - { - id: 'B-C', - source: 'B', - sourceHandle: 'default', - target: 'C', - targetHandle: 'default', - data: { edgeType: 'default' }, - }, - ], - }; - - const result = transformFilterNodesAsEdges({ - nodes: diagram.nodes, - edges: diagram.edges, - workflowContext: 'workflow', - }); - - // Should only have nodes A and C - expect(result.nodes).toEqual([ - { - id: 'A', - data: { - nodeType: 'action', - actionType: 'CODE', - name: 'Step A', - hasNextStepIds: true, - position: { x: 0, y: 0 }, - stepId: 'A', - }, - position: { x: 0, y: 0 }, - }, - { - id: 'C', - data: { - nodeType: 'action', - actionType: 'SEND_EMAIL', - name: 'Step C', - hasNextStepIds: false, - position: { x: 0, y: 300 }, - stepId: 'C', - }, - position: { x: 0, y: 300 }, - }, - ]); - - // Should have one edge with filter data - expect(result.edges).toHaveLength(1); - expect(result.edges[0]).toEqual({ - id: 'A-C-filter-B', - type: 'filter--editable', - source: 'A', - sourceHandle: 'default', - target: 'C', - targetHandle: 'default', - selectable: true, - deletable: true, - data: { - edgeType: 'filter', - stepId: 'B', - name: 'Filter B', - runStatus: undefined, - filterSettings: {}, - }, - }); - }); - - it('should handle multiple filter nodes', () => { - const diagram: WorkflowDiagram = { - nodes: [ - { - id: 'A', - data: { - nodeType: 'action', - actionType: 'CODE', - name: 'Step A', - hasNextStepIds: true, - position: { x: 0, y: 0 }, - stepId: 'A', - }, - position: { x: 0, y: 0 }, - }, - { - id: 'B1', - data: { - nodeType: 'action', - actionType: 'FILTER', - name: 'Filter B1', - hasNextStepIds: true, - position: { x: 0, y: 150 }, - stepId: 'B1', - }, - position: { x: 0, y: 150 }, - }, - { - id: 'C', - data: { - nodeType: 'action', - actionType: 'SEND_EMAIL', - name: 'Step C', - hasNextStepIds: true, - position: { x: 0, y: 300 }, - stepId: 'C', - }, - position: { x: 0, y: 300 }, - }, - { - id: 'B2', - data: { - nodeType: 'action', - actionType: 'FILTER', - name: 'Filter B2', - hasNextStepIds: true, - position: { x: 0, y: 450 }, - stepId: 'B2', - }, - position: { x: 0, y: 450 }, - }, - { - id: 'D', - data: { - nodeType: 'action', - actionType: 'CREATE_RECORD', - name: 'Step D', - hasNextStepIds: true, - position: { x: 0, y: 600 }, - stepId: 'D', - }, - position: { x: 0, y: 600 }, - }, - ], - edges: [ - { - id: 'A-B1', - source: 'A', - sourceHandle: 'default', - target: 'B1', - targetHandle: 'default', - data: { edgeType: 'default' }, - }, - { - id: 'B1-C', - source: 'B1', - sourceHandle: 'default', - target: 'C', - targetHandle: 'default', - data: { edgeType: 'default' }, - }, - { - id: 'C-B2', - source: 'C', - sourceHandle: 'default', - target: 'B2', - targetHandle: 'default', - data: { edgeType: 'default' }, - }, - { - id: 'B2-D', - source: 'B2', - sourceHandle: 'default', - target: 'D', - targetHandle: 'default', - data: { edgeType: 'default' }, - }, - ], - }; - - const result = transformFilterNodesAsEdges({ - nodes: diagram.nodes, - edges: diagram.edges, - workflowContext: 'workflow', - }); - - // Should only have nodes A, C, and D - expect(result.nodes).toHaveLength(3); - expect(result.nodes.map((n) => n.id)).toEqual( - expect.arrayContaining(['A', 'C', 'D']), - ); - - // Should have two edges with filter data - expect(result.edges).toHaveLength(2); - - const edgeAC = result.edges.find( - (e) => e.source === 'A' && e.target === 'C', - ); - expect(edgeAC).toEqual({ - id: 'A-C-filter-B1', - type: 'filter--editable', - source: 'A', - sourceHandle: 'default', - target: 'C', - targetHandle: 'default', - selectable: true, - deletable: true, - data: { - edgeType: 'filter', - name: 'Filter B1', - runStatus: undefined, - stepId: 'B1', - filterSettings: {}, - }, - }); - - const edgeCD = result.edges.find( - (e) => e.source === 'C' && e.target === 'D', - ); - expect(edgeCD).toEqual({ - id: 'C-D-filter-B2', - type: 'filter--editable', - source: 'C', - sourceHandle: 'default', - target: 'D', - targetHandle: 'default', - selectable: true, - deletable: true, - data: { - edgeType: 'filter', - name: 'Filter B2', - runStatus: undefined, - stepId: 'B2', - filterSettings: {}, - }, - }); - }); - - it('should handle filter nodes that are not part of a chain', () => { - const diagram: WorkflowDiagram = { - nodes: [ - { - id: 'A', - data: { - nodeType: 'action', - actionType: 'CODE', - name: 'Step A', - hasNextStepIds: true, - position: { x: 0, y: 0 }, - stepId: 'A', - }, - position: { x: 0, y: 0 }, - }, - { - id: 'B', - data: { - nodeType: 'action', - actionType: 'FILTER', - name: 'Filter B', - hasNextStepIds: true, - position: { x: 0, y: 150 }, - stepId: 'B', - }, - position: { x: 0, y: 150 }, - }, - ], - edges: [ - { - id: 'A-B', - source: 'A', - sourceHandle: 'default', - target: 'B', - targetHandle: 'default', - data: { edgeType: 'default' }, - }, - ], - }; - - const result = transformFilterNodesAsEdges({ - nodes: diagram.nodes, - edges: diagram.edges, - workflowContext: 'workflow', - }); - - // Should only have node A (filter node B is removed) - expect(result.nodes).toEqual([ - { - id: 'A', - data: { - nodeType: 'action', - actionType: 'CODE', - name: 'Step A', - hasNextStepIds: true, - position: { x: 0, y: 0 }, - stepId: 'A', - }, - position: { x: 0, y: 0 }, - }, - ]); - - // Should have no edges (original edge A-B is removed, no new edges created) - expect(result.edges).toEqual([]); - }); - - it('should preserve trigger nodes', () => { - const diagram: WorkflowDiagram = { - nodes: [ - { - id: 'trigger', - data: { - nodeType: 'trigger', - triggerType: 'DATABASE_EVENT', - name: 'Trigger', - hasNextStepIds: true, - position: { x: 0, y: 0 }, - stepId: 'trigger', - }, - position: { x: 0, y: 0 }, - }, - { - id: 'B', - data: { - nodeType: 'action', - actionType: 'FILTER', - name: 'Filter B', - hasNextStepIds: true, - position: { x: 0, y: 150 }, - stepId: 'B', - }, - position: { x: 0, y: 150 }, - }, - { - id: 'C', - data: { - nodeType: 'action', - actionType: 'SEND_EMAIL', - name: 'Step C', - hasNextStepIds: true, - position: { x: 0, y: 300 }, - stepId: 'C', - }, - position: { x: 0, y: 300 }, - }, - ], - edges: [ - { - id: 'trigger-B', - source: 'trigger', - sourceHandle: 'default', - target: 'B', - targetHandle: 'default', - data: { edgeType: 'default' }, - }, - { - id: 'B-C', - source: 'B', - sourceHandle: 'default', - target: 'C', - targetHandle: 'default', - data: { edgeType: 'default' }, - }, - ], - }; - - const result = transformFilterNodesAsEdges({ - nodes: diagram.nodes, - edges: diagram.edges, - workflowContext: 'workflow', - }); - - // Should have trigger and C nodes - expect(result.nodes).toEqual([ - { - id: 'trigger', - data: { - nodeType: 'trigger', - triggerType: 'DATABASE_EVENT', - name: 'Trigger', - hasNextStepIds: true, - position: { x: 0, y: 0 }, - stepId: 'trigger', - }, - position: { x: 0, y: 0 }, - }, - { - id: 'C', - data: { - nodeType: 'action', - actionType: 'SEND_EMAIL', - name: 'Step C', - hasNextStepIds: true, - position: { x: 0, y: 300 }, - stepId: 'C', - }, - position: { x: 0, y: 300 }, - }, - ]); - - // Should have one edge with filter data - expect(result.edges).toEqual([ - { - id: 'trigger-C-filter-B', - type: 'filter--editable', - source: 'trigger', - sourceHandle: 'default', - target: 'C', - targetHandle: 'default', - selectable: true, - deletable: true, - data: { - edgeType: 'filter', - name: 'Filter B', - runStatus: undefined, - stepId: 'B', - filterSettings: {}, - }, - }, - ]); - }); -}); 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 a14e47d1a2b..d418d5027c1 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 @@ -4,14 +4,12 @@ import { } from '@/workflow/types/Workflow'; import { type WorkflowDiagramEdgeData, - type WorkflowDiagramEdgeType, type WorkflowRunDiagram, type WorkflowRunDiagramNode, type WorkflowRunDiagramStepNodeData, } from '@/workflow/workflow-diagram/types/WorkflowDiagram'; import { generateWorkflowDiagram } from '@/workflow/workflow-diagram/utils/generateWorkflowDiagram'; import { isStepNode } from '@/workflow/workflow-diagram/utils/isStepNode'; -import { transformFilterNodesAsEdges } from '@/workflow/workflow-diagram/utils/transformFilterNodesAsEdges'; import { isDefined } from 'twenty-shared/utils'; import { StepStatus, type WorkflowRunStepInfos } from 'twenty-shared/workflow'; @@ -87,11 +85,9 @@ export const generateWorkflowRunDiagram = ({ const stepInfo = stepInfos?.[parentNode.id]; - const edgeType: WorkflowDiagramEdgeType = 'empty-filter--run'; - return { ...edge, - type: edgeType, + type: 'run', data: { ...edge.data, edgeType: 'default', @@ -101,11 +97,10 @@ export const generateWorkflowRunDiagram = ({ }); return { - diagram: transformFilterNodesAsEdges({ + diagram: { nodes: workflowRunDiagramNodes, edges: workflowRunDiagramEdges, - 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 index f488162a4e4..575028b982f 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-diagram/utils/getEdgeTypeBetweenTwoNodes.ts +++ b/packages/twenty-front/src/modules/workflow/workflow-diagram/utils/getEdgeTypeBetweenTwoNodes.ts @@ -8,11 +8,11 @@ export const getEdgeTypeBetweenTwoNodes = ({ }): WorkflowDiagramEdgeType => { switch (workflowContext) { case 'workflow': { - return 'empty-filter--editable'; + return 'editable'; } case 'workflow-version': case 'workflow-run': { - return 'empty-filter--readonly'; + return '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 deleted file mode 100644 index 5502336cc8f..00000000000 --- a/packages/twenty-front/src/modules/workflow/workflow-diagram/utils/getFilterEdgeType.ts +++ /dev/null @@ -1,20 +0,0 @@ -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/getWorkflowDiagramColors.ts b/packages/twenty-front/src/modules/workflow/workflow-diagram/utils/getWorkflowDiagramColors.ts index f02ca4c1e65..738391f873b 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-diagram/utils/getWorkflowDiagramColors.ts +++ b/packages/twenty-front/src/modules/workflow/workflow-diagram/utils/getWorkflowDiagramColors.ts @@ -41,8 +41,7 @@ export const getWorkflowDiagramColors = ({ }, }; } - case 'FAILED': - case 'STOPPED': { + case 'FAILED': { return { selected: { background: theme.adaptiveColors.red1, @@ -60,6 +59,7 @@ export const getWorkflowDiagramColors = ({ }, }; } + case 'STOPPED': case 'SUCCESS': { return { selected: { 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 b4bded07366..00866cdf8e7 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 @@ -2,7 +2,6 @@ import { type WorkflowVersion } from '@/workflow/types/Workflow'; 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'; const EMPTY_DIAGRAM: WorkflowDiagram = { @@ -21,15 +20,9 @@ export const getWorkflowVersionDiagram = ({ return EMPTY_DIAGRAM; } - const diagram = generateWorkflowDiagram({ + return generateWorkflowDiagram({ trigger: workflowVersion.trigger ?? undefined, steps: workflowVersion.steps ?? [], workflowContext, }); - - return transformFilterNodesAsEdges({ - nodes: diagram.nodes, - edges: diagram.edges, - 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 deleted file mode 100644 index da433156989..00000000000 --- a/packages/twenty-front/src/modules/workflow/workflow-diagram/utils/transformFilterNodesAsEdges.ts +++ /dev/null @@ -1,95 +0,0 @@ -import { type WorkflowContext } from '@/workflow/workflow-diagram/types/WorkflowContext'; -import { - type WorkflowDiagramEdge, - 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 = < - T extends WorkflowDiagramNode, - U extends WorkflowDiagramEdge, ->({ - nodes, - edges, - workflowContext, -}: { - nodes: T[]; - edges: U[]; - workflowContext: WorkflowContext; -}): { nodes: T[]; edges: U[] } => { - const filterNodes = nodes.filter( - (node) => - node.data.nodeType === 'action' && - 'actionType' in node.data && - node.data.actionType === 'FILTER', - ); - - if (filterNodes.length === 0) { - return { nodes, edges }; - } - - const resultNodes = nodes.filter( - (node) => !filterNodes.some((filterNode) => filterNode.id === node.id), - ); - - const resultEdges = [...edges]; - const edgesToRemove = new Set(); - const edgesToAdd: typeof edges = []; - - for (const filterNode of filterNodes) { - const incomingEdge = edges.find((edge) => edge.target === filterNode.id); - const outgoingEdge = edges.find((edge) => edge.source === filterNode.id); - - if (isDefined(incomingEdge) && isDefined(outgoingEdge)) { - if ( - filterNode.data.nodeType !== 'action' || - filterNode.data.actionType !== 'FILTER' - ) { - throw new Error('Expected the filter node to be of action type'); - } - - const filterEdgeType = getFilterEdgeType({ workflowContext }); - - const newEdge: U = { - ...incomingEdge, - type: filterEdgeType, - id: `${incomingEdge.source}-${outgoingEdge.target}-filter-${filterNode.id}`, - target: outgoingEdge.target, - selectable: true, - deletable: true, - data: { - ...incomingEdge.data, - edgeType: 'filter', - stepId: filterNode.id, - // TODO: Get the filter settings from the filter node - filterSettings: {}, - name: filterNode.data.name, - runStatus: filterNode.data.runStatus, - }, - }; - - edgesToAdd.push(newEdge); - edgesToRemove.add(incomingEdge.id); - edgesToRemove.add(outgoingEdge.id); - } else { - if (isDefined(incomingEdge)) { - edgesToRemove.add(incomingEdge.id); - } - - if (isDefined(outgoingEdge)) { - edgesToRemove.add(outgoingEdge.id); - } - } - } - - const finalEdges = [ - ...resultEdges.filter((edge) => !edgesToRemove.has(edge.id)), - ...edgesToAdd, - ]; - - return { - nodes: resultNodes, - edges: finalEdges, - }; -}; 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 568fd5d4229..160ca1c8fe5 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 @@ -1,6 +1,3 @@ -import { ActionMenuContext } from '@/action-menu/contexts/ActionMenuContext'; -import { commandMenuNavigationStackState } from '@/command-menu/states/commandMenuNavigationStackState'; -import { useOpenWorkflowEditFilterInCommandMenu } from '@/workflow/workflow-diagram/hooks/useOpenWorkflowEditFilterInCommandMenu'; import { useStartNodeCreation } from '@/workflow/workflow-diagram/hooks/useStartNodeCreation'; import { WorkflowDiagramBaseEdge } from '@/workflow/workflow-diagram/workflow-edges/components/WorkflowDiagramBaseEdge'; import { WorkflowDiagramEdgeButtonGroup } from '@/workflow/workflow-diagram/workflow-edges/components/WorkflowDiagramEdgeButtonGroup'; @@ -12,14 +9,12 @@ import { WORKFLOW_DIAGRAM_EDGE_OPTIONS_CLICK_OUTSIDE_ID } from '@/workflow/workf import { useEdgeState } from '@/workflow/workflow-diagram/workflow-edges/hooks/useEdgeState'; import { type WorkflowDiagramEdgeComponentProps } from '@/workflow/workflow-diagram/workflow-edges/types/WorkflowDiagramEdgeComponentProps'; import { getConnectionOptionsForSourceHandle } from '@/workflow/workflow-diagram/workflow-edges/utils/getConnectionOptionsForSourceHandle'; -import { useCreateStep } from '@/workflow/workflow-steps/hooks/useCreateStep'; import { useDeleteEdge } from '@/workflow/workflow-steps/hooks/useDeleteEdge'; import { useLingui } from '@lingui/react/macro'; import { EdgeLabelRenderer, getBezierPath } from '@xyflow/react'; -import { type MouseEvent, useContext } from 'react'; -import { useSetRecoilState } from 'recoil'; +import { type MouseEvent } from 'react'; import { isDefined } from 'twenty-shared/utils'; -import { IconFilter, IconPlus, IconTrash } from 'twenty-ui/display'; +import { IconPlus, IconTrash } from 'twenty-ui/display'; type WorkflowDiagramDefaultEdgeEditableProps = WorkflowDiagramEdgeComponentProps; @@ -39,8 +34,6 @@ export const WorkflowDiagramDefaultEdgeEditable = ({ }: WorkflowDiagramDefaultEdgeEditableProps) => { const { i18n } = useLingui(); - const { isInRightDrawer } = useContext(ActionMenuContext); - const { isEdgeHovered } = useEdgeState(); const [edgePath, labelX, labelY] = getBezierPath({ @@ -50,8 +43,6 @@ export const WorkflowDiagramDefaultEdgeEditable = ({ targetY, }); - const { createStep } = useCreateStep(); - const { deleteEdge } = useDeleteEdge(); const { startNodeCreation, isNodeCreationStarted } = useStartNodeCreation(); @@ -61,34 +52,6 @@ export const WorkflowDiagramDefaultEdgeEditable = ({ nextStepId: target, }); - const setCommandMenuNavigationStack = useSetRecoilState( - commandMenuNavigationStackState, - ); - - const { openWorkflowEditFilterInCommandMenu } = - useOpenWorkflowEditFilterInCommandMenu(); - - const handleCreateFilter = async () => { - const createdStep = await createStep({ - newStepType: 'FILTER', - parentStepId: source, - nextStepId: target, - }); - - if (!isDefined(createdStep)) { - return; - } - - if (!isInRightDrawer) { - setCommandMenuNavigationStack([]); - } - - openWorkflowEditFilterInCommandMenu({ - stepId: createdStep.id, - stepName: createdStep.name, - }); - }; - const handleNodeButtonClick = () => { startNodeCreation({ parentStepId: source, @@ -153,10 +116,6 @@ export const WorkflowDiagramDefaultEdgeEditable = ({ > asserts data is WorkflowDiagramEdgeData & { edgeType: 'filter' } = ( - data: WorkflowDiagramEdgeData | undefined, -) => { - if (data?.edgeType !== 'filter') { - throw new Error('Edge data must be of type "filter"'); - } -}; - -const StyledConfiguredFilterContainer = styled.div` - height: 26px; - width: 26px; - position: relative; -`; - -const StyledIconButtonGroup = styled(IconButtonGroup)<{ selected?: boolean }>` - pointer-events: all; - - ${({ selected, theme }) => { - if (!selected) return ''; - const colors = getWorkflowDiagramColors({ theme }); - return css` - background-color: ${colors.selected.background}; - border: 1px solid ${colors.selected.borderColor}; - `; - }} -`; - -export const WorkflowDiagramFilterEdgeEditable = ({ - source, - sourceHandleId, - target, - targetHandleId, - sourceY, - sourceX, - targetY, - targetX, - markerStart, - markerEnd, - data, -}: WorkflowDiagramFilterEdgeEditableProps) => { - assertFilterEdgeDataOrThrow(data); - - const { t, i18n } = useLingui(); - - const theme = useTheme(); - - const [edgePath, labelX, labelY] = getBezierPath({ - sourceX, - sourceY, - targetX, - targetY, - }); - - const { isInRightDrawer } = useContext(ActionMenuContext); - - const { deleteStep } = useDeleteStep(); - const { deleteEdge } = useDeleteEdge(); - const { startNodeCreation, isNodeCreationStarted } = useStartNodeCreation(); - - const setCommandMenuNavigationStack = useSetRecoilState( - commandMenuNavigationStackState, - ); - - const { openDropdown } = useOpenDropdown(); - const { closeDropdown } = useCloseDropdown(); - - const { isEdgeHovered } = useEdgeState(); - - const setWorkflowDiagramPanOnDrag = useSetRecoilComponentState( - workflowDiagramPanOnDragComponentState, - ); - - const nodeCreationStarted = isNodeCreationStarted({ - parentStepId: data.stepId, - nextStepId: target, - }); - - const workflowSelectedNode = useRecoilComponentValue( - workflowSelectedNodeComponentState, - ); - - const isFilterNodeSelected = - isNonEmptyString(data.stepId) && workflowSelectedNode === data.stepId; - - const dropdownId = `${WORKFLOW_DIAGRAM_EDGE_OPTIONS_CLICK_OUTSIDE_ID}-${source}-${target}`; - - const isDropdownOpen = useRecoilComponentValue( - isDropdownOpenComponentState, - dropdownId, - ); - - const { openWorkflowEditFilterInCommandMenu } = - useOpenWorkflowEditFilterInCommandMenu(); - - const handleFilterButtonClick = () => { - if (!isInRightDrawer) { - setCommandMenuNavigationStack([]); - } - - openWorkflowEditFilterInCommandMenu({ - stepId: data.stepId, - stepName: data.name, - }); - }; - - const handleRemoveFilterButtonClick = async () => { - closeDropdown(dropdownId); - - await deleteStep(data.stepId); - }; - - const handleAddNodeButtonClick = () => { - closeDropdown(dropdownId); - - startNodeCreation({ - parentStepId: data.stepId, - nextStepId: target, - position: { x: labelX, y: labelY }, - }); - }; - - const handleDeleteBranchClick = async () => { - closeDropdown(dropdownId); - - await deleteEdge({ - source, - target, - }); - }; - - const { filterCounter } = useFilterCounter({ stepId: data.stepId }); - const { unselected } = getWorkflowDiagramColors({ theme }); - - return ( - <> - - - - {isDefined(data?.labelOptions) && ( - - - - )} - - - - - - {isEdgeHovered({ - source, - target, - sourceHandle: sourceHandleId, - targetHandle: targetHandleId, - }) || - isDropdownOpen || - nodeCreationStarted ? ( - { - openDropdown({ - dropdownComponentInstanceIdFromProps: dropdownId, - }); - }, - }, - ]} - selected={isFilterNodeSelected} - /> - ) : ( - - )} - - - } - data-select-disable - dropdownPlacement="bottom-start" - dropdownStrategy="absolute" - dropdownOffset={{ - x: 24, - y: 4, - }} - onOpen={() => { - setWorkflowDiagramPanOnDrag(false); - }} - onClose={() => { - setWorkflowDiagramPanOnDrag(true); - }} - dropdownComponents={ - - - - - - - - } - /> - - - - - ); -}; diff --git a/packages/twenty-front/src/modules/workflow/workflow-diagram/workflow-edges/components/WorkflowDiagramFilterEdgeReadonly.tsx b/packages/twenty-front/src/modules/workflow/workflow-diagram/workflow-edges/components/WorkflowDiagramFilterEdgeReadonly.tsx deleted file mode 100644 index 04769a474d3..00000000000 --- a/packages/twenty-front/src/modules/workflow/workflow-diagram/workflow-edges/components/WorkflowDiagramFilterEdgeReadonly.tsx +++ /dev/null @@ -1,143 +0,0 @@ -import { ActionMenuContext } from '@/action-menu/contexts/ActionMenuContext'; -import { commandMenuNavigationStackState } from '@/command-menu/states/commandMenuNavigationStackState'; -import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue'; -import { useOpenWorkflowViewFilterInCommandMenu } from '@/workflow/workflow-diagram/hooks/useOpenWorkflowViewFilterInCommandMenu'; -import { workflowSelectedNodeComponentState } from '@/workflow/workflow-diagram/states/workflowSelectedNodeComponentState'; -import { type WorkflowDiagramEdgeData } from '@/workflow/workflow-diagram/types/WorkflowDiagram'; -import { getWorkflowDiagramColors } from '@/workflow/workflow-diagram/utils/getWorkflowDiagramColors'; -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 { type WorkflowDiagramEdgeComponentProps } from '@/workflow/workflow-diagram/workflow-edges/types/WorkflowDiagramEdgeComponentProps'; -import { WorkflowStepFilterCounter } from '@/workflow/workflow-steps/workflow-actions/filter-action/components/WorkflowStepFilterCounter'; -import { useFilterCounter } from '@/workflow/workflow-steps/workflow-actions/filter-action/hooks/useFilterCounter'; -import { css, useTheme } from '@emotion/react'; -import styled from '@emotion/styled'; -import { isNonEmptyString } from '@sniptt/guards'; -import { BaseEdge, EdgeLabelRenderer, getBezierPath } from '@xyflow/react'; -import { useContext } from 'react'; -import { useSetRecoilState } from 'recoil'; -import { IconFilter } from 'twenty-ui/display'; -import { IconButtonGroup } from 'twenty-ui/input'; - -type WorkflowDiagramFilterEdgeReadonlyProps = WorkflowDiagramEdgeComponentProps; - -const assertFilterEdgeDataOrThrow: ( - data: WorkflowDiagramEdgeData | undefined, -) => asserts data is WorkflowDiagramEdgeData & { edgeType: 'filter' } = ( - data: WorkflowDiagramEdgeData | undefined, -) => { - if (data?.edgeType !== 'filter') { - throw new Error('Edge data must be of type "filter"'); - } -}; - -const StyledIconButtonGroup = styled(IconButtonGroup)<{ selected?: boolean }>` - pointer-events: all; - - ${({ selected, theme }) => { - if (!selected) return ''; - const colors = getWorkflowDiagramColors({ theme }); - return css` - background-color: ${colors.selected.background}; - border: 1px solid ${colors.selected.borderColor}; - `; - }} -`; - -const StyledConfiguredFilterContainer = styled.div` - height: 26px; - width: 26px; -`; - -export const WorkflowDiagramFilterEdgeReadonly = ({ - sourceX, - sourceY, - targetX, - targetY, - markerStart, - markerEnd, - data, -}: WorkflowDiagramFilterEdgeReadonlyProps) => { - assertFilterEdgeDataOrThrow(data); - - const { isInRightDrawer } = useContext(ActionMenuContext); - - const theme = useTheme(); - - const [edgePath, labelX, labelY] = getBezierPath({ - sourceX, - sourceY, - targetX, - targetY, - }); - - const workflowSelectedNode = useRecoilComponentValue( - workflowSelectedNodeComponentState, - ); - - const isFilterNodeSelected = - isNonEmptyString(data.stepId) && workflowSelectedNode === data.stepId; - - const { openWorkflowViewFilterInCommandMenu } = - useOpenWorkflowViewFilterInCommandMenu(); - - const setCommandMenuNavigationStack = useSetRecoilState( - commandMenuNavigationStackState, - ); - - const handleFilterButtonClick = () => { - if (!isInRightDrawer) { - setCommandMenuNavigationStack([]); - } - - openWorkflowViewFilterInCommandMenu({ - stepId: data.stepId, - stepName: data.name, - }); - }; - - const { filterCounter } = useFilterCounter({ - stepId: data.stepId, - }); - const { unselected } = getWorkflowDiagramColors({ theme }); - - return ( - <> - - - - - - - - - - - - - - ); -}; diff --git a/packages/twenty-front/src/modules/workflow/workflow-diagram/workflow-edges/components/WorkflowDiagramFilterEdgeRun.tsx b/packages/twenty-front/src/modules/workflow/workflow-diagram/workflow-edges/components/WorkflowDiagramFilterEdgeRun.tsx deleted file mode 100644 index 452c847a94a..00000000000 --- a/packages/twenty-front/src/modules/workflow/workflow-diagram/workflow-edges/components/WorkflowDiagramFilterEdgeRun.tsx +++ /dev/null @@ -1,141 +0,0 @@ -import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue'; -import type { WorkflowRunStepStatus } from '@/workflow/types/Workflow'; -import { useOpenWorkflowRunFilterInCommandMenu } from '@/workflow/workflow-diagram/hooks/useOpenWorkflowRunFilterInCommandMenu'; -import { workflowSelectedNodeComponentState } from '@/workflow/workflow-diagram/states/workflowSelectedNodeComponentState'; -import { type WorkflowDiagramEdgeData } from '@/workflow/workflow-diagram/types/WorkflowDiagram'; -import { getWorkflowDiagramColors } from '@/workflow/workflow-diagram/utils/getWorkflowDiagramColors'; -import { WorkflowDiagramEdgeV2Container } from '@/workflow/workflow-diagram/workflow-edges/components/WorkflowDiagramEdgeV2Container'; -import { WorkflowDiagramEdgeV2VisibilityContainer } from '@/workflow/workflow-diagram/workflow-edges/components/WorkflowDiagramEdgeV2VisibilityContainer'; -import { WorkflowRunDiagramBaseEdge } from '@/workflow/workflow-diagram/workflow-edges/components/WorkflowRunDiagramBaseEdge'; -import { WORKFLOW_DIAGRAM_EDGE_OPTIONS_CLICK_OUTSIDE_ID } from '@/workflow/workflow-diagram/workflow-edges/constants/WorkflowDiagramEdgeOptionsClickOutsideId'; -import { type WorkflowDiagramEdgeComponentProps } from '@/workflow/workflow-diagram/workflow-edges/types/WorkflowDiagramEdgeComponentProps'; -import { WorkflowStepFilterCounter } from '@/workflow/workflow-steps/workflow-actions/filter-action/components/WorkflowStepFilterCounter'; -import { useFilterCounter } from '@/workflow/workflow-steps/workflow-actions/filter-action/hooks/useFilterCounter'; -import { css, useTheme } from '@emotion/react'; -import styled from '@emotion/styled'; -import { isNonEmptyString } from '@sniptt/guards'; -import { EdgeLabelRenderer, getBezierPath } from '@xyflow/react'; -import { isDefined } from 'twenty-shared/utils'; -import { IconFilter } from 'twenty-ui/display'; -import { IconButtonGroup } from 'twenty-ui/input'; - -type WorkflowDiagramFilterEdgeRunProps = WorkflowDiagramEdgeComponentProps; - -const assertFilterEdgeDataOrThrow: ( - data: WorkflowDiagramEdgeData | undefined, -) => asserts data is WorkflowDiagramEdgeData & { edgeType: 'filter' } = ( - data: WorkflowDiagramEdgeData | undefined, -) => { - if (data?.edgeType !== 'filter') { - throw new Error('Edge data must be of type "filter"'); - } -}; - -const StyledIconButtonGroup = styled(IconButtonGroup)<{ - selected?: boolean; - runStatus?: WorkflowRunStepStatus; -}>` - pointer-events: all; - - ${({ selected, runStatus, theme }) => { - if (!selected) return ''; - const colors = getWorkflowDiagramColors({ runStatus, theme }); - return css` - background-color: ${colors.selected.background}; - border: 1px solid ${colors.selected.borderColor}; - `; - }} -`; - -const StyledConfiguredFilterContainer = styled.div` - height: 26px; - width: 26px; -`; - -export const WorkflowDiagramFilterEdgeRun = ({ - sourceX, - sourceY, - targetX, - targetY, - data, - markerStart, - markerEnd, -}: WorkflowDiagramFilterEdgeRunProps) => { - assertFilterEdgeDataOrThrow(data); - - const theme = useTheme(); - - const [edgePath, labelX, labelY] = getBezierPath({ - sourceX, - sourceY, - targetX, - targetY, - }); - - const workflowSelectedNode = useRecoilComponentValue( - workflowSelectedNodeComponentState, - ); - - const isFilterNodeSelected = - isNonEmptyString(data.stepId) && workflowSelectedNode === data.stepId; - - const { openWorkflowRunFilterInCommandMenu } = - useOpenWorkflowRunFilterInCommandMenu(); - - const handleFilterButtonClick = () => { - if (!isDefined(data.runStatus)) { - throw new Error('Run status must be set on edge data for workflow runs'); - } - - openWorkflowRunFilterInCommandMenu({ - stepId: data.stepId, - stepName: data.name, - stepExecutionStatus: data.runStatus, - }); - }; - - const { filterCounter } = useFilterCounter({ stepId: data.stepId }); - const { selected } = getWorkflowDiagramColors({ - theme, - runStatus: data.runStatus, - }); - - return ( - <> - - - - - - - - - - - - - - ); -}; diff --git a/packages/twenty-front/src/modules/workflow/workflow-diagram/workflow-edges/constants/WorkflowVisualizerEdgeDefaultConfiguration.ts b/packages/twenty-front/src/modules/workflow/workflow-diagram/workflow-edges/constants/WorkflowVisualizerEdgeDefaultConfiguration.ts index 67b9ab23193..4d3743c329f 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-diagram/workflow-edges/constants/WorkflowVisualizerEdgeDefaultConfiguration.ts +++ b/packages/twenty-front/src/modules/workflow/workflow-diagram/workflow-edges/constants/WorkflowVisualizerEdgeDefaultConfiguration.ts @@ -1,11 +1,11 @@ -import { EDGE_BRANCH_ARROW_MARKER } from '@/workflow/workflow-diagram/workflow-edges/constants/EdgeBranchArrowMarker'; import { type WorkflowDiagramEdge, type WorkflowDiagramEdgeType, } from '@/workflow/workflow-diagram/types/WorkflowDiagram'; +import { EDGE_BRANCH_ARROW_MARKER } from '@/workflow/workflow-diagram/workflow-edges/constants/EdgeBranchArrowMarker'; export const WORKFLOW_VISUALIZER_EDGE_DEFAULT_CONFIGURATION = { - type: 'empty-filter--readonly' satisfies WorkflowDiagramEdgeType, + type: 'readonly' satisfies WorkflowDiagramEdgeType, markerStart: undefined, ...EDGE_BRANCH_ARROW_MARKER.Default, deletable: false, 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 31450bf7a90..dd019adffa1 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 @@ -122,7 +122,8 @@ export const WorkflowRunDiagramStepNode = ({ {capitalize(data.nodeType)} - {data.runStatus === StepStatus.SUCCESS && ( + {(data.runStatus === StepStatus.SUCCESS || + data.runStatus === StepStatus.STOPPED) && ( diff --git a/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/constants/OtherActions.ts b/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/constants/FlowActions.ts similarity index 53% rename from packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/constants/OtherActions.ts rename to packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/constants/FlowActions.ts index 6791bd27dd2..b13f8f591b4 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/constants/OtherActions.ts +++ b/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/constants/FlowActions.ts @@ -1,8 +1,8 @@ import { type WorkflowActionType } from '@/workflow/types/Workflow'; -export const OTHER_ACTIONS: Array<{ +export const FLOW_ACTIONS: Array<{ label: string; - type: Extract; + type: Extract; icon: string; }> = [ { @@ -10,4 +10,9 @@ export const OTHER_ACTIONS: Array<{ type: 'ITERATOR', icon: 'IconRepeat', }, + { + label: 'Filter', + type: 'FILTER', + icon: 'IconFilter', + }, ]; 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/workflow-actions/filter-action/components/WorkflowDropdownStepOutputItems.tsx index 967c2c5ac7b..581d38dd9cb 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/workflow-actions/filter-action/components/WorkflowDropdownStepOutputItems.tsx @@ -18,6 +18,7 @@ import { type StepOutputSchemaV2 } from '@/workflow/workflow-variables/types/Ste import { extractRawVariableNamePart } from '@/workflow/workflow-variables/utils/extractRawVariableNamePart'; import { getCurrentSubStepFromPath } from '@/workflow/workflow-variables/utils/getCurrentSubStepFromPath'; import { getStepHeaderLabel } from '@/workflow/workflow-variables/utils/getStepHeaderLabel'; +import { getStepItemIcon } from '@/workflow/workflow-variables/utils/getStepItemIcon'; import { getVariableTemplateFromPath } from '@/workflow/workflow-variables/utils/getVariableTemplateFromPath'; import { searchVariableThroughOutputSchemaV2 } from '@/workflow/workflow-variables/utils/searchVariableThroughOutputSchemaV2'; import { useLingui } from '@lingui/react/macro'; @@ -185,7 +186,7 @@ export const WorkflowDropdownStepOutputItems = ({ subStepObjectMetadataItem.labelSingular .toLowerCase() .includes(searchInputValue.toLowerCase()) - : true; + : isDefined(displayedSubStepObject); const objectLabel = subStepObjectMetadataItem?.labelSingular; @@ -236,7 +237,15 @@ export const WorkflowDropdownStepOutputItems = ({ onClick={() => handleSelectField(key)} text={subStep.label || key} hasSubMenu={!subStep.isLeaf} - LeftIcon={subStep.icon ? getIcon(subStep.icon) : undefined} + LeftIcon={ + subStep.icon + ? getIcon(subStep.icon) + : getIcon( + getStepItemIcon({ + itemType: subStep.type, + }), + ) + } contextualText={ subStep.isLeaf ? subStep?.value?.toString() : undefined } diff --git a/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/filter-action/components/WorkflowStepFilterCounter.tsx b/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/filter-action/components/WorkflowStepFilterCounter.tsx deleted file mode 100644 index b97f99ec5ee..00000000000 --- a/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/filter-action/components/WorkflowStepFilterCounter.tsx +++ /dev/null @@ -1,37 +0,0 @@ -import styled from '@emotion/styled'; - -const StyledFilterCounter = styled.div<{ - backgroundColor: string; - textColor: string; -}>` - align-items: center; - background: ${({ backgroundColor }) => backgroundColor}; - border-radius: 50%; - color: ${({ textColor }) => textColor}; - display: flex; - font-size: ${({ theme }) => theme.font.size.xxs}; - font-weight: ${({ theme }) => theme.font.weight.semiBold}; - height: 12px; - justify-content: center; - width: 12px; - left: -3px; - position: absolute; - top: -3px; - z-index: 1; -`; - -type WorkflowStepFilterCounterProps = { - backgroundColor: string; - textColor: string; - counter: number; -}; - -export const WorkflowStepFilterCounter = ({ - backgroundColor, - textColor, - counter, -}: WorkflowStepFilterCounterProps) => ( - - {counter} - -); diff --git a/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/filter-action/components/__stories__/WorkflowStepFilterCounter.stories.tsx b/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/filter-action/components/__stories__/WorkflowStepFilterCounter.stories.tsx deleted file mode 100644 index 1ca44ccf46b..00000000000 --- a/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/filter-action/components/__stories__/WorkflowStepFilterCounter.stories.tsx +++ /dev/null @@ -1,28 +0,0 @@ -import { WorkflowStepFilterCounter } from '@/workflow/workflow-steps/workflow-actions/filter-action/components/WorkflowStepFilterCounter'; -import { type Meta, type StoryObj } from '@storybook/react'; -import { expect, within } from '@storybook/test'; -import { ComponentDecorator } from 'twenty-ui/testing'; -import { THEME_LIGHT } from 'twenty-ui/theme'; - -const meta: Meta = { - title: 'Modules/Workflow/Actions/Filter/WorkflowStepFilterCounter', - component: WorkflowStepFilterCounter, - decorators: [ComponentDecorator], -}; - -export default meta; - -type Story = StoryObj; - -export const Default: Story = { - args: { - counter: 1, - backgroundColor: THEME_LIGHT.border.color.strong, - textColor: THEME_LIGHT.font.color.inverted, - }, - play: async ({ canvasElement }) => { - const canvas = within(canvasElement); - - expect(await canvas.findByText('1')).toBeVisible(); - }, -}; diff --git a/packages/twenty-front/src/modules/workflow/workflow-variables/components/WorkflowVariablesDropdownStepItems.tsx b/packages/twenty-front/src/modules/workflow/workflow-variables/components/WorkflowVariablesDropdownStepItems.tsx index f63191ac2fb..8b40cff6fab 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-variables/components/WorkflowVariablesDropdownStepItems.tsx +++ b/packages/twenty-front/src/modules/workflow/workflow-variables/components/WorkflowVariablesDropdownStepItems.tsx @@ -93,11 +93,10 @@ export const WorkflowVariablesDropdownStepItems = ({ .includes(searchInputValue.toLowerCase()) : true; + const objectLabel = displayedSubStepObjectMetadata?.labelSingular; const shouldDisplaySubStepObject = shouldDisplayRecordObjects && isObjectFoundThroughSearch; - const objectLabel = displayedSubStepObjectMetadata?.labelSingular; - return ( { }); }); }); - - describe('with filter steps', () => { - it('should delete the filter step when deleting edge from trigger to target through filter', async () => { - const mockStepsWithFilter = [ - { - id: 'step-1', - type: WorkflowActionType.FORM, - settings: { - errorHandlingOptions: { - continueOnFailure: { value: false }, - retryOnFailure: { value: false }, - }, - }, - nextStepIds: ['step-2'], - }, - { - id: 'step-2', - type: WorkflowActionType.SEND_EMAIL, - settings: { - errorHandlingOptions: { - continueOnFailure: { value: false }, - retryOnFailure: { value: false }, - }, - }, - nextStepIds: [], - }, - { - id: 'filter-step', - type: WorkflowActionType.FILTER, - settings: { - errorHandlingOptions: { - continueOnFailure: { value: false }, - retryOnFailure: { value: false }, - }, - }, - nextStepIds: ['step-2'], - }, - ] as WorkflowAction[]; - - const mockTriggerWithFilter = { - type: WorkflowTriggerType.MANUAL, - settings: {}, - nextStepIds: ['step-1', 'filter-step'], - }; - - const mockWorkflowVersionWithFilter = { - id: mockWorkflowVersionId, - trigger: mockTriggerWithFilter, - steps: mockStepsWithFilter, - status: 'DRAFT', - } as WorkflowVersionWorkspaceEntity; - - workflowCommonWorkspaceService.getWorkflowVersionOrFail.mockResolvedValue( - mockWorkflowVersionWithFilter, - ); - - const result = await service.deleteWorkflowVersionEdge({ - source: TRIGGER_STEP_ID, - target: 'step-2', - workflowVersionId: mockWorkflowVersionId, - workspaceId: mockWorkspaceId, - }); - - expect( - workflowCommonWorkspaceService.getWorkflowVersionOrFail, - ).toHaveBeenCalledWith({ - workflowVersionId: mockWorkflowVersionId, - workspaceId: mockWorkspaceId, - }); - - expect( - mockWorkflowVersionWorkspaceRepository.update, - ).toHaveBeenCalledWith(mockWorkflowVersionId, { - trigger: { - ...mockTriggerWithFilter, - nextStepIds: ['step-1'], - }, - steps: mockStepsWithFilter.filter( - (step) => step.id !== 'filter-step', - ), - }); - - expect(result).toEqual({ - triggerNextStepIds: ['step-1'], - stepsNextStepIds: { - 'step-1': ['step-2'], - 'step-2': [], - }, - }); - }); - it('should delete the filter step when deleting edge from step to target through filter', async () => { - const mockStepsWithFilter = [ - { - id: 'step-1', - type: WorkflowActionType.FORM, - settings: { - errorHandlingOptions: { - continueOnFailure: { value: false }, - retryOnFailure: { value: false }, - }, - }, - nextStepIds: ['step-2', 'filter-step'], - }, - { - id: 'step-2', - type: WorkflowActionType.SEND_EMAIL, - settings: { - errorHandlingOptions: { - continueOnFailure: { value: false }, - retryOnFailure: { value: false }, - }, - }, - nextStepIds: [], - }, - { - id: 'step-3', - type: WorkflowActionType.SEND_EMAIL, - settings: { - errorHandlingOptions: { - continueOnFailure: { value: false }, - retryOnFailure: { value: false }, - }, - }, - nextStepIds: [], - }, - { - id: 'filter-step', - type: WorkflowActionType.FILTER, - settings: { - errorHandlingOptions: { - continueOnFailure: { value: false }, - retryOnFailure: { value: false }, - }, - }, - nextStepIds: ['step-3'], - }, - ] as WorkflowAction[]; - - const mockWorkflowVersionWithFilter = { - id: mockWorkflowVersionId, - trigger: mockTrigger, - steps: mockStepsWithFilter, - status: 'DRAFT', - } as WorkflowVersionWorkspaceEntity; - - workflowCommonWorkspaceService.getWorkflowVersionOrFail.mockResolvedValue( - mockWorkflowVersionWithFilter, - ); - - const result = await service.deleteWorkflowVersionEdge({ - source: 'step-1', - target: 'step-3', - workflowVersionId: mockWorkflowVersionId, - workspaceId: mockWorkspaceId, - }); - - expect( - workflowCommonWorkspaceService.getWorkflowVersionOrFail, - ).toHaveBeenCalledWith({ - workflowVersionId: mockWorkflowVersionId, - workspaceId: mockWorkspaceId, - }); - - expect( - mockWorkflowVersionWorkspaceRepository.update, - ).toHaveBeenCalledWith(mockWorkflowVersionId, { - steps: mockStepsWithFilter - .map((step) => { - if (step.id === 'step-1') { - return { - ...step, - nextStepIds: ['step-2'], - }; - } - - return step; - }) - .filter((step) => step.id !== 'filter-step'), - }); - - expect(result).toEqual({ - triggerNextStepIds: ['step-1'], - stepsNextStepIds: { - 'step-1': ['step-2'], - 'step-2': [], - 'step-3': [], - }, - }); - }); - }); }); }); diff --git a/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-version-edge/workflow-version-edge.workspace-service.ts b/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-version-edge/workflow-version-edge.workspace-service.ts index 4f6243366a4..5347101a46f 100644 --- a/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-version-edge/workflow-version-edge.workspace-service.ts +++ b/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-version-edge/workflow-version-edge.workspace-service.ts @@ -222,6 +222,16 @@ export class WorkflowVersionEdgeWorkspaceService { ); } + if ( + sourceStep.nextStepIds?.includes(target) && + !isDefined(sourceConnectionOptions) + ) { + return computeWorkflowVersionStepChanges({ + trigger, + steps, + }); + } + const { updatedSourceStep, shouldPersist } = isDefined( sourceConnectionOptions, ) @@ -364,12 +374,9 @@ export class WorkflowVersionEdgeWorkspaceService { } if (!trigger.nextStepIds?.includes(target)) { - return this.handleFilterBetweenTriggerAndTarget({ + return computeWorkflowVersionStepChanges({ trigger, steps, - target, - workflowVersionId: workflowVersion.id, - workflowVersionRepository, }); } @@ -416,24 +423,13 @@ export class WorkflowVersionEdgeWorkspaceService { ); } - // TODO: Remove this once we start using filters as regular steps - const isIteratorWithLoopTarget = - isDefined(sourceConnectionOptions) && - sourceConnectionOptions.connectedStepType === - WorkflowActionType.ITERATOR && - sourceConnectionOptions.settings.isConnectedToLoop; - if ( !sourceStep.nextStepIds?.includes(target) && - !isIteratorWithLoopTarget + !isDefined(sourceConnectionOptions) ) { - return await this.handleFilterBetweenSourceAndTarget({ + return computeWorkflowVersionStepChanges({ trigger, steps, - sourceStep, - target, - workflowVersionId: workflowVersion.id, - workflowVersionRepository, }); } @@ -480,109 +476,6 @@ export class WorkflowVersionEdgeWorkspaceService { }); } - private async handleFilterBetweenTriggerAndTarget({ - trigger, - steps, - target, - workflowVersionId, - workflowVersionRepository, - }: { - trigger: WorkflowTrigger; - steps: WorkflowAction[]; - target: string; - workflowVersionId: string; - workflowVersionRepository: WorkspaceRepository; - }): Promise { - const filterBetweenTriggerAndTarget = this.findFilterBetweenNodes({ - steps, - sourceNextStepIds: trigger.nextStepIds, - target, - }); - - if (!isDefined(filterBetweenTriggerAndTarget)) { - return computeWorkflowVersionStepChanges({ - trigger, - steps, - }); - } - - const updatedTrigger = { - ...trigger, - nextStepIds: trigger.nextStepIds?.filter( - (nextStepId: string) => nextStepId !== filterBetweenTriggerAndTarget.id, - ), - }; - - const updatedSteps = steps.filter( - (step) => step.id !== filterBetweenTriggerAndTarget.id, - ); - - await workflowVersionRepository.update(workflowVersionId, { - trigger: updatedTrigger, - steps: updatedSteps, - }); - - return computeWorkflowVersionStepChanges({ - trigger: updatedTrigger, - steps: updatedSteps, - }); - } - - private async handleFilterBetweenSourceAndTarget({ - trigger, - steps, - sourceStep, - target, - workflowVersionRepository, - workflowVersionId, - }: { - trigger: WorkflowTrigger | null; - steps: WorkflowAction[]; - sourceStep: WorkflowAction; - target: string; - workflowVersionRepository: WorkspaceRepository; - workflowVersionId: string; - }): Promise { - const filterBetweenSourceAndTarget = this.findFilterBetweenNodes({ - steps, - sourceNextStepIds: sourceStep.nextStepIds, - target, - }); - - if (!isDefined(filterBetweenSourceAndTarget)) { - return computeWorkflowVersionStepChanges({ - trigger, - steps, - }); - } - - const updatedSourceStep = { - ...sourceStep, - nextStepIds: sourceStep.nextStepIds?.filter( - (nextStepId: string) => nextStepId !== filterBetweenSourceAndTarget.id, - ), - }; - - const updatedSteps = steps - .map((step) => { - if (step.id === sourceStep.id) { - return updatedSourceStep; - } - - return step; - }) - .filter((step) => step.id !== filterBetweenSourceAndTarget.id); - - await workflowVersionRepository.update(workflowVersionId, { - steps: updatedSteps, - }); - - return computeWorkflowVersionStepChanges({ - trigger, - steps: updatedSteps, - }); - } - private buildUpdatedSourceStepWithOptions({ sourceStep, target, @@ -650,22 +543,4 @@ export class WorkflowVersionEdgeWorkspaceService { }; } } - - private findFilterBetweenNodes({ - steps, - sourceNextStepIds, - target, - }: { - steps: WorkflowAction[]; - sourceNextStepIds: string[] | undefined; - target: string; - }) { - const nextStepFilters = steps.filter( - (step) => - sourceNextStepIds?.includes(step.id) && - step.type === WorkflowActionType.FILTER, - ); - - return nextStepFilters.find((step) => step.nextStepIds?.includes(target)); - } } diff --git a/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-version-step/utils/__tests__/remove-step.spec.ts b/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-version-step/utils/__tests__/remove-step.spec.ts index 09f0f3b8a10..7651493ed35 100644 --- a/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-version-step/utils/__tests__/remove-step.spec.ts +++ b/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-version-step/utils/__tests__/remove-step.spec.ts @@ -151,100 +151,6 @@ describe('removeStep', () => { }); }); - it('should remove step child that is a filter', () => { - const step1 = createMockAction('1', ['2']); - const step2 = createMockAction('2', ['3']); - const step3 = { - id: '3', - name: 'Step 3', - type: WorkflowActionType.FILTER, - nextStepIds: ['4'], - } as WorkflowAction; - const step4 = createMockAction('4'); - - const result = removeStep({ - existingTrigger: mockTrigger, - existingSteps: [step1, step2, step3, step4], - stepIdToDelete: '2', - stepToDeleteChildrenIds: ['3'], - }); - - expect(result.updatedTrigger).toEqual(mockTrigger); - expect(result.updatedSteps).toEqual([ - { ...step1, nextStepIds: ['4'] }, - step4, - ]); - }); - - it('should remove trigger children that is a filter', () => { - const step1 = { - id: '1', - name: 'Step 1', - type: WorkflowActionType.FILTER, - nextStepIds: ['2'], - } as WorkflowAction; - const step2 = createMockAction('2', ['3']); - const step3 = createMockAction('3'); - - const result = removeStep({ - existingTrigger: mockTrigger, - existingSteps: [step1, step2, step3], - stepIdToDelete: TRIGGER_STEP_ID, - stepToDeleteChildrenIds: ['1'], - }); - - expect(result.updatedTrigger).toEqual(null); - expect(result.updatedSteps).toEqual([step2, step3]); - }); - - it('should remove filter step if it has no children', () => { - const step1 = { - id: '1', - name: 'Step 1', - type: WorkflowActionType.FILTER, - nextStepIds: ['2'], - } as WorkflowAction; - const step2 = createMockAction('2', ['3']); - const step3 = { - id: '3', - name: 'Step 3', - type: WorkflowActionType.FILTER, - nextStepIds: ['4'], - } as WorkflowAction; - const step4 = createMockAction('4'); - - const result = removeStep({ - existingTrigger: mockTrigger, - existingSteps: [step1, step2, step3, step4], - stepIdToDelete: '4', - }); - - expect(result.updatedTrigger).toEqual(mockTrigger); - expect(result.updatedSteps).toEqual([step1, { ...step2, nextStepIds: [] }]); - }); - - it('should remove filter step if it is the last step', () => { - const step1 = { - id: '1', - name: 'Step 1', - type: WorkflowActionType.FILTER, - nextStepIds: ['2'], - } as WorkflowAction; - const step2 = createMockAction('2'); - - const result = removeStep({ - existingTrigger: mockTrigger, - existingSteps: [step1, step2], - stepIdToDelete: '2', - }); - - expect(result.updatedTrigger).toEqual({ - ...mockTrigger, - nextStepIds: [], - }); - expect(result.updatedSteps).toEqual([]); - }); - it('should remove trigger if steps are null', () => { const result = removeStep({ existingTrigger: { ...mockTrigger, nextStepIds: [] }, @@ -253,7 +159,7 @@ describe('removeStep', () => { }); expect(result.updatedTrigger).toEqual(null); - expect(result.updatedSteps).toEqual([]); + expect(result.updatedSteps).toEqual(null); }); it('should handle removing a step that is part of iteratorLoopStepIds', () => { diff --git a/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-version-step/utils/remove-step.ts b/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-version-step/utils/remove-step.ts index c2ad1109b73..4ac65b5c39f 100644 --- a/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-version-step/utils/remove-step.ts +++ b/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-version-step/utils/remove-step.ts @@ -29,7 +29,7 @@ const computeUpdatedNextStepIds = ({ ]; }; -const removeOneStep = ({ +export const removeStep = ({ existingTrigger, existingSteps, stepIdToDelete, @@ -39,11 +39,15 @@ const removeOneStep = ({ existingSteps: WorkflowAction[] | null; stepIdToDelete: string; stepToDeleteChildrenIds?: string[]; -}): { - updatedSteps: WorkflowAction[]; - updatedTrigger: WorkflowTrigger | null; - removedStepIds: string[]; -} => { +}) => { + if (stepIdToDelete === TRIGGER_STEP_ID) { + return { + updatedSteps: existingSteps, + updatedTrigger: null, + removedStepIds: [TRIGGER_STEP_ID], + }; + } + const updatedSteps = existingSteps ?.filter((step) => step.id !== stepIdToDelete) @@ -104,131 +108,3 @@ const removeOneStep = ({ removedStepIds: [stepIdToDelete], }; }; - -const removeRegularStep = ({ - existingTrigger, - existingSteps, - stepIdToDelete, - stepToDeleteChildrenIds, -}: { - existingTrigger: WorkflowTrigger | null; - existingSteps: WorkflowAction[] | null; - stepIdToDelete: string; - stepToDeleteChildrenIds?: string[]; -}): { - updatedSteps: WorkflowAction[]; - updatedTrigger: WorkflowTrigger | null; - removedStepIds: string[]; -} => { - let { updatedSteps, updatedTrigger, removedStepIds } = removeOneStep({ - existingTrigger, - existingSteps, - stepIdToDelete, - stepToDeleteChildrenIds, - }); - - for (const stepId of stepToDeleteChildrenIds ?? []) { - const step = existingSteps?.find((step) => step.id === stepId); - - if (step?.type === WorkflowActionType.FILTER) { - const { - updatedSteps: stepsAfterRemovingChildFilter, - updatedTrigger: triggerAfterRemovingChildFilter, - removedStepIds: removedStepIdsAfterRemovingChildFilter, - } = removeOneStep({ - existingTrigger: updatedTrigger, - existingSteps: updatedSteps, - stepIdToDelete: stepId, - stepToDeleteChildrenIds: step.nextStepIds, - }); - - updatedSteps = stepsAfterRemovingChildFilter; - updatedTrigger = triggerAfterRemovingChildFilter; - removedStepIds = [ - ...removedStepIds, - ...removedStepIdsAfterRemovingChildFilter, - ]; - } - } - - for (const step of updatedSteps) { - if ( - step?.type === WorkflowActionType.FILTER && - (!isDefined(step?.nextStepIds) || step.nextStepIds?.length === 0) - ) { - const { - updatedSteps: stepsAfterRemovingFilterWithoutChildren, - updatedTrigger: triggerAfterRemovingFilterWithoutChildren, - removedStepIds: removedStepIdsAfterRemovingFilterWithoutChildren, - } = removeOneStep({ - existingTrigger: updatedTrigger, - existingSteps: updatedSteps, - stepIdToDelete: step.id, - stepToDeleteChildrenIds: step.nextStepIds, - }); - - updatedSteps = stepsAfterRemovingFilterWithoutChildren; - updatedTrigger = triggerAfterRemovingFilterWithoutChildren; - removedStepIds = [ - ...removedStepIds, - ...removedStepIdsAfterRemovingFilterWithoutChildren, - ]; - } - } - - return { - updatedSteps, - updatedTrigger, - removedStepIds, - }; -}; - -const removeTrigger = ({ - existingSteps, - triggerChildrenIds, -}: { - existingSteps: WorkflowAction[] | null; - triggerChildrenIds?: string[]; -}) => { - const stepIdsToRemove = - triggerChildrenIds?.filter((id) => { - const step = existingSteps?.find((step) => step.id === id); - - return step?.type === WorkflowActionType.FILTER; - }) ?? []; - - const updatedSteps = - existingSteps?.filter((step) => !stepIdsToRemove.includes(step.id)) ?? []; - - return { - updatedSteps, - updatedTrigger: null, - removedStepIds: [TRIGGER_STEP_ID, ...stepIdsToRemove], - }; -}; - -export const removeStep = ({ - existingTrigger, - existingSteps, - stepIdToDelete, - stepToDeleteChildrenIds, -}: { - existingTrigger: WorkflowTrigger | null; - existingSteps: WorkflowAction[] | null; - stepIdToDelete: string; - stepToDeleteChildrenIds?: string[]; -}) => { - if (stepIdToDelete === TRIGGER_STEP_ID) { - return removeTrigger({ - existingSteps, - triggerChildrenIds: stepToDeleteChildrenIds, - }); - } else { - return removeRegularStep({ - existingTrigger, - existingSteps, - stepIdToDelete, - stepToDeleteChildrenIds, - }); - } -};