diff --git a/packages/twenty-front/src/modules/command-menu/pages/workflow/action/components/CommandMenuWorkflowSelectActionContent.tsx b/packages/twenty-front/src/modules/command-menu/pages/workflow/action/components/CommandMenuWorkflowSelectActionContent.tsx
index 1aafec1f9..670494287 100644
--- a/packages/twenty-front/src/modules/command-menu/pages/workflow/action/components/CommandMenuWorkflowSelectActionContent.tsx
+++ b/packages/twenty-front/src/modules/command-menu/pages/workflow/action/components/CommandMenuWorkflowSelectActionContent.tsx
@@ -1,9 +1,15 @@
-import { WorkflowWithCurrentVersion } from '@/workflow/types/Workflow';
+import { useRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentStateV2';
+import {
+ WorkflowActionType,
+ WorkflowWithCurrentVersion,
+} from '@/workflow/types/Workflow';
import { RightDrawerStepListContainer } from '@/workflow/workflow-steps/components/RightDrawerWorkflowSelectStepContainer';
import { RightDrawerWorkflowSelectStepTitle } from '@/workflow/workflow-steps/components/RightDrawerWorkflowSelectStepTitle';
import { useCreateStep } from '@/workflow/workflow-steps/hooks/useCreateStep';
+import { workflowInsertStepIdsComponentState } from '@/workflow/workflow-steps/states/workflowInsertStepIdsComponentState';
import { RECORD_ACTIONS } from '@/workflow/workflow-steps/workflow-actions/constants/RecordActions';
import { useFilteredOtherActions } from '@/workflow/workflow-steps/workflow-actions/hooks/useFilteredOtherActions';
+import { isDefined } from 'twenty-shared/utils';
import { useIcons } from 'twenty-ui/display';
import { MenuItemCommand } from 'twenty-ui/navigation';
@@ -13,11 +19,36 @@ export const CommandMenuWorkflowSelectActionContent = ({
workflow: WorkflowWithCurrentVersion;
}) => {
const { getIcon } = useIcons();
+
const { createStep } = useCreateStep({
workflow,
});
const filteredOtherActions = useFilteredOtherActions();
+ const [workflowInsertStepIds, setWorkflowInsertStepIds] =
+ useRecoilComponentStateV2(workflowInsertStepIdsComponentState);
+
+ const handleCreateStep = async (actionType: WorkflowActionType) => {
+ const { parentStepId, nextStepId } = workflowInsertStepIds;
+
+ if (!isDefined(parentStepId)) {
+ throw new Error(
+ 'No parentStepId. Please select a parent step to create from.',
+ );
+ }
+
+ await createStep({
+ newStepType: actionType,
+ parentStepId,
+ nextStepId,
+ });
+
+ setWorkflowInsertStepIds({
+ parentStepId: undefined,
+ nextStepId: undefined,
+ });
+ };
+
return (
@@ -28,7 +59,7 @@ export const CommandMenuWorkflowSelectActionContent = ({
key={action.type}
LeftIcon={getIcon(action.icon)}
text={action.label}
- onClick={() => createStep(action.type)}
+ onClick={() => handleCreateStep(action.type)}
/>
))}
@@ -39,7 +70,7 @@ export const CommandMenuWorkflowSelectActionContent = ({
key={action.type}
LeftIcon={getIcon(action.icon)}
text={action.label}
- onClick={() => createStep(action.type)}
+ onClick={() => handleCreateStep(action.type)}
/>
))}
diff --git a/packages/twenty-front/src/modules/workflow/types/Workflow.ts b/packages/twenty-front/src/modules/workflow/types/Workflow.ts
index 762afb843..4d9e1d287 100644
--- a/packages/twenty-front/src/modules/workflow/types/Workflow.ts
+++ b/packages/twenty-front/src/modules/workflow/types/Workflow.ts
@@ -10,6 +10,8 @@ import {
workflowDeleteRecordActionSchema,
workflowDeleteRecordActionSettingsSchema,
workflowExecutorOutputSchema,
+ workflowFilterActionSchema,
+ workflowFilterActionSettingsSchema,
workflowFindRecordsActionSchema,
workflowFindRecordsActionSettingsSchema,
workflowFormActionSchema,
@@ -48,6 +50,9 @@ export type WorkflowDeleteRecordActionSettings = z.infer<
export type WorkflowFindRecordsActionSettings = z.infer<
typeof workflowFindRecordsActionSettingsSchema
>;
+export type WorkflowFilterActionSettings = z.infer<
+ typeof workflowFilterActionSettingsSchema
+>;
export type WorkflowFormActionSettings = z.infer<
typeof workflowFormActionSettingsSchema
>;
@@ -68,6 +73,7 @@ export type WorkflowDeleteRecordAction = z.infer<
export type WorkflowFindRecordsAction = z.infer<
typeof workflowFindRecordsActionSchema
>;
+export type WorkflowFilterAction = z.infer;
export type WorkflowFormAction = z.infer;
export type WorkflowHttpRequestAction = z.infer<
typeof workflowHttpRequestActionSchema
@@ -86,6 +92,7 @@ export type WorkflowAction =
| WorkflowUpdateRecordAction
| WorkflowDeleteRecordAction
| WorkflowFindRecordsAction
+ | WorkflowFilterAction
| WorkflowFormAction
| WorkflowHttpRequestAction
| WorkflowAiAgentAction;
diff --git a/packages/twenty-front/src/modules/workflow/validation-schemas/workflowSchema.ts b/packages/twenty-front/src/modules/workflow/validation-schemas/workflowSchema.ts
index 5ba7dc7e2..078e21421 100644
--- a/packages/twenty-front/src/modules/workflow/validation-schemas/workflowSchema.ts
+++ b/packages/twenty-front/src/modules/workflow/validation-schemas/workflowSchema.ts
@@ -138,6 +138,13 @@ export const workflowAiAgentActionSettingsSchema =
}),
});
+export const workflowFilterActionSettingsSchema =
+ baseWorkflowActionSettingsSchema.extend({
+ input: z.object({
+ filter: z.record(z.any()),
+ }),
+ });
+
// Action schemas
export const workflowCodeActionSchema = baseWorkflowActionSchema.extend({
type: z.literal('CODE'),
@@ -190,6 +197,11 @@ export const workflowAiAgentActionSchema = baseWorkflowActionSchema.extend({
settings: workflowAiAgentActionSettingsSchema,
});
+export const workflowFilterActionSchema = baseWorkflowActionSchema.extend({
+ type: z.literal('FILTER'),
+ settings: workflowFilterActionSettingsSchema,
+});
+
// Combined action schema
export const workflowActionSchema = z.discriminatedUnion('type', [
workflowCodeActionSchema,
@@ -201,6 +213,7 @@ export const workflowActionSchema = z.discriminatedUnion('type', [
workflowFormActionSchema,
workflowHttpRequestActionSchema,
workflowAiAgentActionSchema,
+ workflowFilterActionSchema,
]);
// Trigger schemas
diff --git a/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowDiagramDefaultEdge.tsx b/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowDiagramDefaultEdge.tsx
index 8d5f87d3a..b279c1ec5 100644
--- a/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowDiagramDefaultEdge.tsx
+++ b/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowDiagramDefaultEdge.tsx
@@ -50,8 +50,10 @@ export const WorkflowDiagramDefaultEdge = ({
) : (
`
- padding: ${({ theme }) => theme.spacing(1)};
- pointer-events: all;
- ${({ labelX, labelY }) => css`
- transform: translate(-50%, -50%) translate(${labelX}px, ${labelY}px);
- `}
- position: absolute;
-`;
-
-const StyledOpacityOverlay = styled.div<{ shouldDisplay: boolean }>`
- opacity: ${({ shouldDisplay }) => (shouldDisplay ? 1 : 0)};
- position: relative;
-`;
+import { useCreateStep } from '@/workflow/workflow-steps/hooks/useCreateStep';
+import { useDeleteStep } from '@/workflow/workflow-steps/hooks/useDeleteStep';
+import { isDefined } from 'twenty-shared/utils';
type WorkflowDiagramEdgeV2Props = {
labelX: number;
labelY: number;
+ stepId: string | undefined;
parentStepId: string;
nextStepId: string;
+ filter: Record | undefined;
};
export const WorkflowDiagramEdgeV2 = ({
labelX,
labelY,
+ stepId,
parentStepId,
nextStepId,
+ filter,
}: WorkflowDiagramEdgeV2Props) => {
- const { openDropdown } = useOpenDropdown();
- const { closeDropdown } = useCloseDropdown();
+ const workflowVisualizerWorkflowId = useRecoilComponentValueV2(
+ workflowVisualizerWorkflowIdComponentState,
+ );
+ const workflow = useWorkflowWithCurrentVersion(workflowVisualizerWorkflowId);
+ assertWorkflowWithCurrentVersionIsDefined(workflow);
+
+ const { createStep } = useCreateStep({ workflow });
+ const { deleteStep } = useDeleteStep({ workflow });
const { startNodeCreation } = useStartNodeCreation();
- const [hovered, setHovered] = useState(false);
-
- const setWorkflowDiagramPanOnDrag = useSetRecoilComponentStateV2(
- workflowDiagramPanOnDragComponentState,
- );
-
- const workflowInsertStepIds = useRecoilComponentValueV2(
- workflowInsertStepIdsComponentState,
- );
-
- const isSelected =
- workflowInsertStepIds.parentStepId === parentStepId &&
- workflowInsertStepIds.nextStepId === nextStepId;
-
- const dropdownId = `${WORKFLOW_DIAGRAM_EDGE_OPTIONS_CLICK_OUTSIDE_ID}-${parentStepId}-${nextStepId}`;
-
- const isDropdownOpen = useRecoilComponentValueV2(
- isDropdownOpenComponentStateV2,
- dropdownId,
- );
-
return (
- setHovered(true)}
- onMouseLeave={() => setHovered(false)}
- >
-
- {},
- },
- {
- Icon: IconDotsVertical,
- onClick: () => {
- openDropdown({
- dropdownComponentInstanceIdFromProps: dropdownId,
- });
- },
- },
- ]}
- />
+ stepId={stepId}
+ parentStepId={parentStepId}
+ nextStepId={nextStepId}
+ filter={filter}
+ onCreateFilter={() => {
+ return createStep({
+ newStepType: 'FILTER',
+ parentStepId,
+ nextStepId,
+ });
+ }}
+ onDeleteFilter={() => {
+ if (!isDefined(stepId)) {
+ throw new Error(
+ 'Step ID must be configured for the edge when rendering a filter',
+ );
+ }
- }
- data-select-disable
- dropdownPlacement="bottom-start"
- dropdownStrategy="absolute"
- dropdownOffset={{
- x: 0,
- y: 4,
- }}
- onOpen={() => {
- setWorkflowDiagramPanOnDrag(false);
- }}
- onClose={() => {
- setWorkflowDiagramPanOnDrag(true);
- }}
- dropdownComponents={
-
-
-
-
- }
- />
-
-
+ return deleteStep(stepId);
+ }}
+ onCreateNode={() => {
+ if (isDefined(filter)) {
+ startNodeCreation({ parentStepId: stepId, nextStepId });
+ } else {
+ startNodeCreation({ parentStepId, nextStepId });
+ }
+ }}
+ />
);
};
diff --git a/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowDiagramEdgeV2Content.tsx b/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowDiagramEdgeV2Content.tsx
new file mode 100644
index 000000000..208d4490b
--- /dev/null
+++ b/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowDiagramEdgeV2Content.tsx
@@ -0,0 +1,209 @@
+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 { GenericDropdownContentWidth } from '@/ui/layout/dropdown/constants/GenericDropdownContentWidth';
+import { useCloseDropdown } from '@/ui/layout/dropdown/hooks/useCloseDropdown';
+import { useOpenDropdown } from '@/ui/layout/dropdown/hooks/useOpenDropdown';
+import { isDropdownOpenComponentStateV2 } from '@/ui/layout/dropdown/states/isDropdownOpenComponentStateV2';
+import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
+import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2';
+import { WORKFLOW_DIAGRAM_EDGE_OPTIONS_CLICK_OUTSIDE_ID } from '@/workflow/workflow-diagram/constants/WorkflowDiagramEdgeOptionsClickOutsideId';
+import { workflowDiagramPanOnDragComponentState } from '@/workflow/workflow-diagram/states/workflowDiagramPanOnDragComponentState';
+import { workflowInsertStepIdsComponentState } from '@/workflow/workflow-steps/states/workflowInsertStepIdsComponentState';
+import { css } from '@emotion/react';
+import styled from '@emotion/styled';
+import { isNonEmptyString } from '@sniptt/guards';
+import { useState } from 'react';
+import { isDefined } from 'twenty-shared/utils';
+import {
+ IconDotsVertical,
+ IconFilter,
+ IconFilterPlus,
+ IconFilterX,
+ IconGitBranchDeleted,
+ IconPlus,
+} from 'twenty-ui/display';
+import { IconButtonGroup } from 'twenty-ui/input';
+import { MenuItem } from 'twenty-ui/navigation';
+
+const StyledIconButtonGroup = styled(IconButtonGroup)`
+ pointer-events: all;
+`;
+
+const StyledRoundedIconButtonGroup = styled(IconButtonGroup)`
+ border-radius: 50px;
+ overflow: hidden;
+ pointer-events: all;
+`;
+
+const StyledContainer = styled.div<{ labelX: number; labelY: number }>`
+ padding: ${({ theme }) => theme.spacing(1)};
+ pointer-events: all;
+ ${({ labelX, labelY }) => css`
+ transform: translate(-50%, -50%) translate(${labelX}px, ${labelY}px);
+ `}
+ position: absolute;
+`;
+
+const StyledOpacityOverlay = styled.div<{ shouldDisplay: boolean }>`
+ opacity: ${({ shouldDisplay }) => (shouldDisplay ? 1 : 0)};
+ position: relative;
+`;
+
+type WorkflowDiagramEdgeV2ContentProps = {
+ labelX: number;
+ labelY: number;
+ stepId: string | undefined;
+ parentStepId: string;
+ nextStepId: string;
+ filter: Record | undefined;
+ onCreateFilter: () => Promise;
+ onDeleteFilter: () => Promise;
+ onCreateNode: () => void;
+};
+
+export const WorkflowDiagramEdgeV2Content = ({
+ labelX,
+ labelY,
+ stepId,
+ parentStepId,
+ nextStepId,
+ filter,
+ onCreateFilter,
+ onDeleteFilter,
+ onCreateNode,
+}: WorkflowDiagramEdgeV2ContentProps) => {
+ const { openDropdown } = useOpenDropdown();
+ const { closeDropdown } = useCloseDropdown();
+
+ const [hovered, setHovered] = useState(false);
+
+ const setWorkflowDiagramPanOnDrag = useSetRecoilComponentStateV2(
+ workflowDiagramPanOnDragComponentState,
+ );
+
+ const workflowInsertStepIds = useRecoilComponentValueV2(
+ workflowInsertStepIdsComponentState,
+ );
+
+ const isSelected =
+ workflowInsertStepIds.nextStepId === nextStepId &&
+ (workflowInsertStepIds.parentStepId === parentStepId ||
+ (isNonEmptyString(stepId) &&
+ workflowInsertStepIds.parentStepId === stepId));
+
+ const dropdownId = `${WORKFLOW_DIAGRAM_EDGE_OPTIONS_CLICK_OUTSIDE_ID}-${parentStepId}-${nextStepId}`;
+
+ const isDropdownOpen = useRecoilComponentValueV2(
+ isDropdownOpenComponentStateV2,
+ dropdownId,
+ );
+
+ const handleCreateFilter = async () => {
+ await onCreateFilter();
+
+ closeDropdown(dropdownId);
+ setHovered(false);
+ };
+
+ return (
+ setHovered(true)}
+ onMouseLeave={() => setHovered(false)}
+ >
+
+ {isDefined(filter) && !hovered && !isDropdownOpen && !isSelected ? (
+
+ ) : (
+ {
+ handleCreateFilter();
+ },
+ },
+ {
+ Icon: IconDotsVertical,
+ onClick: () => {
+ openDropdown({
+ dropdownComponentInstanceIdFromProps: dropdownId,
+ });
+ },
+ },
+ ]}
+ />
+ )}
+
+ }
+ data-select-disable
+ dropdownPlacement="bottom-start"
+ dropdownStrategy="absolute"
+ dropdownOffset={{
+ x: 0,
+ y: 4,
+ }}
+ onOpen={() => {
+ setWorkflowDiagramPanOnDrag(false);
+ }}
+ onClose={() => {
+ setWorkflowDiagramPanOnDrag(true);
+ }}
+ dropdownComponents={
+
+
+ {}}
+ />
+ {
+ closeDropdown(dropdownId);
+ setHovered(false);
+
+ onDeleteFilter();
+ }}
+ />
+ {
+ closeDropdown(dropdownId);
+ setHovered(false);
+
+ onCreateNode();
+ }}
+ />
+ {}}
+ />
+
+
+ }
+ />
+
+
+ );
+};
diff --git a/packages/twenty-front/src/modules/workflow/workflow-diagram/components/__stories__/WorkflowDiagramEdgeV2.stories.tsx b/packages/twenty-front/src/modules/workflow/workflow-diagram/components/__stories__/WorkflowDiagramEdgeV2Content.stories.tsx
similarity index 81%
rename from packages/twenty-front/src/modules/workflow/workflow-diagram/components/__stories__/WorkflowDiagramEdgeV2.stories.tsx
rename to packages/twenty-front/src/modules/workflow/workflow-diagram/components/__stories__/WorkflowDiagramEdgeV2Content.stories.tsx
index 5bd3f2380..d52dc3ce4 100644
--- a/packages/twenty-front/src/modules/workflow/workflow-diagram/components/__stories__/WorkflowDiagramEdgeV2.stories.tsx
+++ b/packages/twenty-front/src/modules/workflow/workflow-diagram/components/__stories__/WorkflowDiagramEdgeV2Content.stories.tsx
@@ -1,17 +1,17 @@
import { WorkflowVisualizerComponentInstanceContext } from '@/workflow/workflow-diagram/states/contexts/WorkflowVisualizerComponentInstanceContext';
import { Meta, StoryObj } from '@storybook/react';
-import { expect, userEvent, waitFor, within } from '@storybook/test';
+import { expect, fn, userEvent, waitFor, within } from '@storybook/test';
import '@xyflow/react/dist/style.css';
import {
ComponentDecorator,
getCanvasElementForDropdownTesting,
} from 'twenty-ui/testing';
import { ReactflowDecorator } from '~/testing/decorators/ReactflowDecorator';
-import { WorkflowDiagramEdgeV2 } from '../WorkflowDiagramEdgeV2';
+import { WorkflowDiagramEdgeV2Content } from '../WorkflowDiagramEdgeV2Content';
-const meta: Meta = {
- title: 'Modules/Workflow/WorkflowDiagramEdgeV2',
- component: WorkflowDiagramEdgeV2,
+const meta: Meta = {
+ title: 'Modules/Workflow/WorkflowDiagramEdgeV2Content',
+ component: WorkflowDiagramEdgeV2Content,
decorators: [
ComponentDecorator,
ReactflowDecorator,
@@ -35,11 +35,14 @@ const meta: Meta = {
labelY: 0,
parentStepId: 'parent-step-id',
nextStepId: 'next-step-id',
+ onCreateFilter: fn(),
+ onDeleteFilter: fn(),
+ onCreateNode: fn(),
},
};
export default meta;
-type Story = StoryObj;
+type Story = StoryObj;
export const ButtonsAppearOnHover: Story = {
play: async ({ canvasElement }) => {
@@ -57,7 +60,7 @@ export const ButtonsAppearOnHover: Story = {
};
export const CreateFilter: Story = {
- play: async ({ canvasElement }) => {
+ play: async ({ canvasElement, args }) => {
const canvas = within(canvasElement);
const buttons = await canvas.findAllByRole('button');
@@ -71,12 +74,14 @@ export const CreateFilter: Story = {
userEvent.click(filterButton);
- // TODO: Assert we created a filter
+ await waitFor(() => {
+ expect(args.onCreateFilter).toHaveBeenCalledTimes(1);
+ });
},
};
export const AddNodeAction: Story = {
- play: async ({ canvasElement }) => {
+ play: async ({ canvasElement, args }) => {
const canvas = within(canvasElement);
const buttons = await canvas.findAllByRole('button');
@@ -99,6 +104,10 @@ export const AddNodeAction: Story = {
await waitFor(() => {
expect(canvas.queryByText('Add Node')).not.toBeInTheDocument();
});
+
+ await waitFor(() => {
+ expect(args.onCreateNode).toHaveBeenCalledTimes(1);
+ });
},
};
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 a86c26e88..131a64147 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
@@ -69,6 +69,8 @@ export type WorkflowRunDiagramNodeData = Exclude<
> & { runStatus: WorkflowDiagramRunStatus };
export type EdgeData = {
+ stepId?: string;
+ filter?: Record;
shouldDisplayEdgeOptions?: boolean;
};
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
new file mode 100644
index 000000000..d28b26a7b
--- /dev/null
+++ b/packages/twenty-front/src/modules/workflow/workflow-diagram/utils/__tests__/transformFilterNodesAsEdges.test.ts
@@ -0,0 +1,333 @@
+import { 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' },
+ position: { x: 0, y: 0 },
+ },
+ {
+ id: 'C',
+ data: {
+ nodeType: 'action',
+ actionType: 'SEND_EMAIL',
+ name: 'Step C',
+ },
+ position: { x: 0, y: 300 },
+ },
+ ],
+ edges: [
+ {
+ id: 'A-C',
+ source: 'A',
+ target: 'C',
+ data: { stepId: 'A', shouldDisplayEdgeOptions: true },
+ },
+ ],
+ };
+
+ const result = transformFilterNodesAsEdges(diagram);
+
+ 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' },
+ position: { x: 0, y: 0 },
+ },
+ {
+ id: 'B',
+ data: { nodeType: 'action', actionType: 'FILTER', name: 'Filter B' },
+ position: { x: 0, y: 150 },
+ },
+ {
+ id: 'C',
+ data: {
+ nodeType: 'action',
+ actionType: 'SEND_EMAIL',
+ name: 'Step C',
+ },
+ position: { x: 0, y: 300 },
+ },
+ ],
+ edges: [
+ {
+ id: 'A-B',
+ source: 'A',
+ target: 'B',
+ data: { stepId: 'A', shouldDisplayEdgeOptions: true },
+ },
+ {
+ id: 'B-C',
+ source: 'B',
+ target: 'C',
+ data: { stepId: 'B', shouldDisplayEdgeOptions: true },
+ },
+ ],
+ };
+
+ const result = transformFilterNodesAsEdges(diagram);
+
+ // Should only have nodes A and C
+ expect(result.nodes).toEqual([
+ {
+ id: 'A',
+ data: { nodeType: 'action', actionType: 'CODE', name: 'Step A' },
+ position: { x: 0, y: 0 },
+ },
+ {
+ id: 'C',
+ data: { nodeType: 'action', actionType: 'SEND_EMAIL', name: 'Step 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',
+ source: 'A',
+ target: 'C',
+ data: {
+ shouldDisplayEdgeOptions: true,
+ stepId: 'B',
+ filter: { nodeType: 'action', actionType: 'FILTER', name: 'Filter B' },
+ },
+ });
+ });
+
+ it('should handle multiple filter nodes', () => {
+ const diagram: WorkflowDiagram = {
+ nodes: [
+ {
+ id: 'A',
+ data: { nodeType: 'action', actionType: 'CODE', name: 'Step A' },
+ position: { x: 0, y: 0 },
+ },
+ {
+ id: 'B1',
+ data: { nodeType: 'action', actionType: 'FILTER', name: 'Filter B1' },
+ position: { x: 0, y: 150 },
+ },
+ {
+ id: 'C',
+ data: {
+ nodeType: 'action',
+ actionType: 'SEND_EMAIL',
+ name: 'Step C',
+ },
+ position: { x: 0, y: 300 },
+ },
+ {
+ id: 'B2',
+ data: { nodeType: 'action', actionType: 'FILTER', name: 'Filter B2' },
+ position: { x: 0, y: 450 },
+ },
+ {
+ id: 'D',
+ data: {
+ nodeType: 'action',
+ actionType: 'CREATE_RECORD',
+ name: 'Step D',
+ },
+ position: { x: 0, y: 600 },
+ },
+ ],
+ edges: [
+ {
+ id: 'A-B1',
+ source: 'A',
+ target: 'B1',
+ data: { stepId: 'A', shouldDisplayEdgeOptions: true },
+ },
+ {
+ id: 'B1-C',
+ source: 'B1',
+ target: 'C',
+ data: { stepId: 'B1', shouldDisplayEdgeOptions: true },
+ },
+ {
+ id: 'C-B2',
+ source: 'C',
+ target: 'B2',
+ data: { stepId: 'C', shouldDisplayEdgeOptions: true },
+ },
+ {
+ id: 'B2-D',
+ source: 'B2',
+ target: 'D',
+ data: { stepId: 'B2', shouldDisplayEdgeOptions: true },
+ },
+ ],
+ };
+
+ const result = transformFilterNodesAsEdges(diagram);
+
+ // 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',
+ source: 'A',
+ target: 'C',
+ data: {
+ stepId: 'B1',
+ shouldDisplayEdgeOptions: true,
+ filter: { nodeType: 'action', actionType: 'FILTER', name: 'Filter B1' },
+ },
+ });
+
+ const edgeCD = result.edges.find(
+ (e) => e.source === 'C' && e.target === 'D',
+ );
+ expect(edgeCD).toEqual({
+ id: 'C-D-filter-B2',
+ source: 'C',
+ target: 'D',
+ data: {
+ stepId: 'B2',
+ shouldDisplayEdgeOptions: true,
+ filter: { nodeType: 'action', actionType: 'FILTER', name: 'Filter B2' },
+ },
+ });
+ });
+
+ 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' },
+ position: { x: 0, y: 0 },
+ },
+ {
+ id: 'B',
+ data: { nodeType: 'action', actionType: 'FILTER', name: 'Filter B' },
+ position: { x: 0, y: 150 },
+ },
+ ],
+ edges: [
+ {
+ id: 'A-B',
+ source: 'A',
+ target: 'B',
+ data: { stepId: 'A', shouldDisplayEdgeOptions: true },
+ },
+ ],
+ };
+
+ const result = transformFilterNodesAsEdges(diagram);
+
+ // Should only have node A (filter node B is removed)
+ expect(result.nodes).toEqual([
+ {
+ id: 'A',
+ data: { nodeType: 'action', actionType: 'CODE', name: 'Step 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',
+ },
+ position: { x: 0, y: 0 },
+ },
+ {
+ id: 'B',
+ data: { nodeType: 'action', actionType: 'FILTER', name: 'Filter B' },
+ position: { x: 0, y: 150 },
+ },
+ {
+ id: 'C',
+ data: {
+ nodeType: 'action',
+ actionType: 'SEND_EMAIL',
+ name: 'Step C',
+ },
+ position: { x: 0, y: 300 },
+ },
+ ],
+ edges: [
+ {
+ id: 'trigger-B',
+ source: 'trigger',
+ target: 'B',
+ data: { stepId: 'trigger', shouldDisplayEdgeOptions: true },
+ },
+ {
+ id: 'B-C',
+ source: 'B',
+ target: 'C',
+ data: { stepId: 'B', shouldDisplayEdgeOptions: true },
+ },
+ ],
+ };
+
+ const result = transformFilterNodesAsEdges(diagram);
+
+ // Should have trigger and C nodes
+ expect(result.nodes).toEqual([
+ {
+ id: 'trigger',
+ data: {
+ nodeType: 'trigger',
+ triggerType: 'DATABASE_EVENT',
+ name: 'Trigger',
+ },
+ position: { x: 0, y: 0 },
+ },
+ {
+ id: 'C',
+ data: { nodeType: 'action', actionType: 'SEND_EMAIL', name: 'Step C' },
+ position: { x: 0, y: 300 },
+ },
+ ]);
+
+ // Should have one edge with filter data
+ expect(result.edges).toEqual([
+ {
+ id: 'trigger-C-filter-B',
+ source: 'trigger',
+ target: 'C',
+ data: {
+ stepId: 'B',
+ shouldDisplayEdgeOptions: true,
+ filter: {
+ nodeType: 'action',
+ actionType: 'FILTER',
+ name: 'Filter B',
+ },
+ },
+ },
+ ]);
+ });
+});
diff --git a/packages/twenty-front/src/modules/workflow/workflow-diagram/utils/addEdgeOptions.ts b/packages/twenty-front/src/modules/workflow/workflow-diagram/utils/addEdgeOptions.ts
index ac2cd3e27..f37e00111 100644
--- a/packages/twenty-front/src/modules/workflow/workflow-diagram/utils/addEdgeOptions.ts
+++ b/packages/twenty-front/src/modules/workflow/workflow-diagram/utils/addEdgeOptions.ts
@@ -6,9 +6,14 @@ export const addEdgeOptions = ({
}: WorkflowDiagram): WorkflowDiagram => {
return {
nodes,
- edges: edges.map((edge) => ({
- ...edge,
- data: { shouldDisplayEdgeOptions: true },
- })),
+ edges: edges.map((edge) => {
+ return {
+ ...edge,
+ data: {
+ ...edge.data,
+ shouldDisplayEdgeOptions: true,
+ },
+ };
+ }),
};
};
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 34b92df7c..b6b98b2a4 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
@@ -10,9 +10,10 @@ import {
WorkflowRunDiagramNode,
WorkflowRunDiagramStepNodeData,
} from '@/workflow/workflow-diagram/types/WorkflowDiagram';
-import { isDefined } from 'twenty-shared/utils';
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';
export const generateWorkflowRunDiagram = ({
trigger,
@@ -38,7 +39,9 @@ export const generateWorkflowRunDiagram = ({
}
| undefined = undefined;
- const workflowDiagram = generateWorkflowDiagram({ trigger, steps });
+ const workflowDiagram = transformFilterNodesAsEdges(
+ generateWorkflowDiagram({ trigger, steps }),
+ );
let skippedExecution = false;
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 9ac6a65a7..e50ff47e1 100644
--- a/packages/twenty-front/src/modules/workflow/workflow-diagram/utils/getWorkflowVersionDiagram.ts
+++ b/packages/twenty-front/src/modules/workflow/workflow-diagram/utils/getWorkflowVersionDiagram.ts
@@ -1,6 +1,7 @@
import { WorkflowVersion } from '@/workflow/types/Workflow';
import { 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 = {
@@ -15,8 +16,10 @@ export const getWorkflowVersionDiagram = (
return EMPTY_DIAGRAM;
}
- return generateWorkflowDiagram({
- trigger: workflowVersion.trigger ?? undefined,
- steps: workflowVersion.steps ?? [],
- });
+ return transformFilterNodesAsEdges(
+ generateWorkflowDiagram({
+ trigger: workflowVersion.trigger ?? undefined,
+ steps: workflowVersion.steps ?? [],
+ }),
+ );
};
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
new file mode 100644
index 000000000..67584b881
--- /dev/null
+++ b/packages/twenty-front/src/modules/workflow/workflow-diagram/utils/transformFilterNodesAsEdges.ts
@@ -0,0 +1,66 @@
+import { WorkflowDiagram } from '@/workflow/workflow-diagram/types/WorkflowDiagram';
+import { isDefined } from 'twenty-shared/utils';
+
+export const transformFilterNodesAsEdges = ({
+ nodes,
+ edges,
+}: WorkflowDiagram): WorkflowDiagram => {
+ 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)) {
+ const newEdge = {
+ ...incomingEdge,
+ id: `${incomingEdge.source}-${outgoingEdge.target}-filter-${filterNode.id}`,
+ target: outgoingEdge.target,
+ data: {
+ ...incomingEdge.data,
+ stepId: filterNode.id,
+ filter: filterNode.data,
+ },
+ };
+
+ 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-steps/components/WorkflowRunStepNodeDetail.tsx b/packages/twenty-front/src/modules/workflow/workflow-steps/components/WorkflowRunStepNodeDetail.tsx
index 9929c7711..862423663 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
@@ -200,6 +200,11 @@ export const WorkflowRunStepNodeDetail = ({
/>
);
}
+ case 'FILTER': {
+ throw new Error(
+ "The Filter action isn't meant to be displayed as a node.",
+ );
+ }
}
}
}
diff --git a/packages/twenty-front/src/modules/workflow/workflow-steps/components/WorkflowStepDetail.tsx b/packages/twenty-front/src/modules/workflow/workflow-steps/components/WorkflowStepDetail.tsx
index e2e316a13..05b20e820 100644
--- a/packages/twenty-front/src/modules/workflow/workflow-steps/components/WorkflowStepDetail.tsx
+++ b/packages/twenty-front/src/modules/workflow/workflow-steps/components/WorkflowStepDetail.tsx
@@ -184,6 +184,11 @@ export const WorkflowStepDetail = ({
/>
);
}
+ case 'FILTER': {
+ throw new Error(
+ "The Filter action isn't meant to be displayed as a node.",
+ );
+ }
default:
return assertUnreachable(
diff --git a/packages/twenty-front/src/modules/workflow/workflow-steps/hooks/__tests__/useCreateStep.test.tsx b/packages/twenty-front/src/modules/workflow/workflow-steps/hooks/__tests__/useCreateStep.test.tsx
index 07a9c60b6..b633921da 100644
--- a/packages/twenty-front/src/modules/workflow/workflow-steps/hooks/__tests__/useCreateStep.test.tsx
+++ b/packages/twenty-front/src/modules/workflow/workflow-steps/hooks/__tests__/useCreateStep.test.tsx
@@ -1,5 +1,4 @@
import { WorkflowWithCurrentVersion } from '@/workflow/types/Workflow';
-import { workflowInsertStepIdsComponentState } from '@/workflow/workflow-steps/states/workflowInsertStepIdsComponentState';
import { renderHook } from '@testing-library/react';
import { RecoilRoot } from 'recoil';
import { WorkflowVisualizerComponentInstanceContext } from '../../../workflow-diagram/states/contexts/WorkflowVisualizerComponentInstanceContext';
@@ -30,16 +29,7 @@ const wrapper = ({ children }: { children: React.ReactNode }) => {
'workflow-visualizer-instance-id';
return (
- {
- set(
- workflowInsertStepIdsComponentState.atomFamily({
- instanceId: workflowVisualizerComponentInstanceId,
- }),
- { parentStepId: 'parent-step-id', nextStepId: undefined },
- );
- }}
- >
+
{
wrapper,
},
);
- await result.current.createStep('CODE');
+ await result.current.createStep({
+ newStepType: 'CODE',
+ parentStepId: 'parent-step-id',
+ nextStepId: undefined,
+ });
expect(mockCreateWorkflowVersionStep).toHaveBeenCalled();
});
diff --git a/packages/twenty-front/src/modules/workflow/workflow-steps/hooks/useCreateStep.ts b/packages/twenty-front/src/modules/workflow/workflow-steps/hooks/useCreateStep.ts
index 2236b97e4..43019717d 100644
--- a/packages/twenty-front/src/modules/workflow/workflow-steps/hooks/useCreateStep.ts
+++ b/packages/twenty-front/src/modules/workflow/workflow-steps/hooks/useCreateStep.ts
@@ -7,10 +7,7 @@ import {
} from '@/workflow/types/Workflow';
import { workflowSelectedNodeComponentState } from '@/workflow/workflow-diagram/states/workflowSelectedNodeComponentState';
import { useCreateWorkflowVersionStep } from '@/workflow/workflow-steps/hooks/useCreateWorkflowVersionStep';
-import { workflowInsertStepIdsComponentState } from '@/workflow/workflow-steps/states/workflowInsertStepIdsComponentState';
-import { isDefined } from 'twenty-shared/utils';
import { useState } from 'react';
-import { useRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentStateV2';
export const useCreateStep = ({
workflow,
@@ -26,12 +23,17 @@ export const useCreateStep = ({
workflowLastCreatedStepIdComponentState,
);
- const [workflowInsertStepIds, setWorkflowInsertStepIds] =
- useRecoilComponentStateV2(workflowInsertStepIdsComponentState);
-
const { getUpdatableWorkflowVersion } = useGetUpdatableWorkflowVersion();
- const createStep = async (newStepType: WorkflowStepType) => {
+ const createStep = async ({
+ newStepType,
+ parentStepId,
+ nextStepId,
+ }: {
+ newStepType: WorkflowStepType;
+ parentStepId: string;
+ nextStepId: string | undefined;
+ }) => {
if (isLoading === true) {
return;
}
@@ -39,20 +41,14 @@ export const useCreateStep = ({
setIsLoading(true);
try {
- if (!isDefined(workflowInsertStepIds.parentStepId)) {
- throw new Error(
- 'No parentStepId. Please select a parent step to create from.',
- );
- }
-
const workflowVersionId = await getUpdatableWorkflowVersion(workflow);
const createdStep = (
await createWorkflowVersionStep({
workflowVersionId,
stepType: newStepType,
- parentStepId: workflowInsertStepIds.parentStepId,
- nextStepId: workflowInsertStepIds.nextStepId,
+ parentStepId,
+ nextStepId,
})
)?.data?.createWorkflowVersionStep;
@@ -62,10 +58,6 @@ export const useCreateStep = ({
setWorkflowSelectedNode(createdStep.id);
setWorkflowLastCreatedStepId(createdStep.id);
- setWorkflowInsertStepIds({
- parentStepId: undefined,
- nextStepId: undefined,
- });
} finally {
setIsLoading(false);
}
diff --git a/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/hooks/__tests__/useWorkflowActionHeader.test.tsx b/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/hooks/__tests__/useWorkflowActionHeader.test.tsx
index 8626b3118..d7e402184 100644
--- a/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/hooks/__tests__/useWorkflowActionHeader.test.tsx
+++ b/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/hooks/__tests__/useWorkflowActionHeader.test.tsx
@@ -1,5 +1,9 @@
import {
- workflowActionSchema,
+ WorkflowFormAction,
+ WorkflowHttpRequestAction,
+ WorkflowSendEmailAction,
+} from '@/workflow/types/Workflow';
+import {
workflowFormActionSettingsSchema,
workflowHttpRequestActionSettingsSchema,
workflowSendEmailActionSettingsSchema,
@@ -34,7 +38,7 @@ describe('useWorkflowActionHeader', () => {
describe('when action name is not defined', () => {
it('should return default title', () => {
- const action = workflowActionSchema.parse({
+ const action = {
id: '1',
name: '',
type: 'HTTP_REQUEST',
@@ -52,7 +56,7 @@ describe('useWorkflowActionHeader', () => {
},
}),
valid: true,
- });
+ } satisfies WorkflowHttpRequestAction;
const { result } = renderHook(() =>
useWorkflowActionHeader({
@@ -71,7 +75,7 @@ describe('useWorkflowActionHeader', () => {
describe('when action name is defined', () => {
it('should return the action name', () => {
- const action = workflowActionSchema.parse({
+ const action = {
id: '1',
name: 'Test Action',
type: 'HTTP_REQUEST',
@@ -89,7 +93,7 @@ describe('useWorkflowActionHeader', () => {
},
}),
valid: true,
- });
+ } satisfies WorkflowHttpRequestAction;
const { result } = renderHook(() =>
useWorkflowActionHeader({
@@ -108,7 +112,7 @@ describe('useWorkflowActionHeader', () => {
describe('when action type is defined', () => {
it('should return default title for HTTP request action', () => {
- const action = workflowActionSchema.parse({
+ const action = {
id: '1',
name: '',
type: 'HTTP_REQUEST',
@@ -126,7 +130,7 @@ describe('useWorkflowActionHeader', () => {
},
}),
valid: true,
- });
+ } satisfies WorkflowHttpRequestAction;
const { result } = renderHook(() =>
useWorkflowActionHeader({
@@ -143,7 +147,7 @@ describe('useWorkflowActionHeader', () => {
});
it('should return default title for form action', () => {
- const action = workflowActionSchema.parse({
+ const action = {
id: '1',
name: '',
type: 'FORM',
@@ -165,7 +169,7 @@ describe('useWorkflowActionHeader', () => {
},
}),
valid: true,
- });
+ } satisfies WorkflowFormAction;
const { result } = renderHook(() =>
useWorkflowActionHeader({
@@ -182,7 +186,7 @@ describe('useWorkflowActionHeader', () => {
});
it('should return default title for email action', () => {
- const action = workflowActionSchema.parse({
+ const action = {
id: '1',
name: '',
type: 'SEND_EMAIL',
@@ -200,7 +204,7 @@ describe('useWorkflowActionHeader', () => {
},
}),
valid: true,
- });
+ } satisfies WorkflowSendEmailAction;
const { result } = renderHook(() =>
useWorkflowActionHeader({
diff --git a/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/utils/getActionHeaderTypeOrThrow.ts b/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/utils/getActionHeaderTypeOrThrow.ts
index f86b119e1..34e42800d 100644
--- a/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/utils/getActionHeaderTypeOrThrow.ts
+++ b/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/utils/getActionHeaderTypeOrThrow.ts
@@ -17,6 +17,11 @@ export const getActionHeaderTypeOrThrow = (actionType: WorkflowActionType) => {
return msg`HTTP Request`;
case 'AI_AGENT':
return msg`AI Agent`;
+ case 'FILTER': {
+ throw new Error(
+ "The Filter action isn't meant to be displayed as a node.",
+ );
+ }
default:
assertUnreachable(actionType, `Unsupported action type: ${actionType}`);
diff --git a/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/utils/getActionIconColorOrThrow.ts b/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/utils/getActionIconColorOrThrow.ts
index 7ae50765f..cb8d14b64 100644
--- a/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/utils/getActionIconColorOrThrow.ts
+++ b/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/utils/getActionIconColorOrThrow.ts
@@ -23,6 +23,11 @@ export const getActionIconColorOrThrow = ({
return theme.color.blue;
case 'AI_AGENT':
return theme.color.pink;
+ case 'FILTER': {
+ throw new Error(
+ "The Filter action isn't meant to be displayed as a node.",
+ );
+ }
default:
assertUnreachable(actionType, `Unsupported action type: ${actionType}`);
}
diff --git a/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-step/workflow-version-step.workspace-service.ts b/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-step/workflow-version-step.workspace-service.ts
index e0067f024..2067b585e 100644
--- a/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-step/workflow-version-step.workspace-service.ts
+++ b/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-step/workflow-version-step.workspace-service.ts
@@ -576,6 +576,20 @@ export class WorkflowVersionStepWorkspaceService {
},
};
}
+ case WorkflowActionType.FILTER: {
+ return {
+ id: newStepId,
+ name: 'Filter',
+ type: WorkflowActionType.FILTER,
+ valid: false,
+ settings: {
+ ...BASE_STEP_DEFINITION,
+ input: {
+ filter: {},
+ },
+ },
+ };
+ }
case WorkflowActionType.HTTP_REQUEST: {
return {
id: newStepId,