diff --git a/package.json b/package.json
index 134d5c4b8..cebb2a269 100644
--- a/package.json
+++ b/package.json
@@ -62,7 +62,7 @@
"@types/nodemailer": "^6.4.14",
"@types/passport-microsoft": "^1.0.3",
"@wyw-in-js/vite": "^0.5.3",
- "@xyflow/react": "^12.3.5",
+ "@xyflow/react": "^12.4.2",
"add": "^2.0.6",
"addressparser": "^1.0.1",
"afterframe": "^1.0.2",
diff --git a/packages/twenty-e2e-testing/tests/demo/demo_basic.spec.ts b/packages/twenty-e2e-testing/tests/demo/demo_basic.spec.ts
index b405c464a..fe5f62513 100644
--- a/packages/twenty-e2e-testing/tests/demo/demo_basic.spec.ts
+++ b/packages/twenty-e2e-testing/tests/demo/demo_basic.spec.ts
@@ -1,15 +1,16 @@
import { expect, test } from '@playwright/test';
-test('Check if demo account is working properly @demo-only', async ({
- page,
-}) => {
- await page.goto('https://app.twenty-next.com/');
- await page.getByRole('button', { name: 'Continue with Email' }).click();
- await page.getByRole('button', { name: 'Continue', exact: true }).click();
- await page.getByRole('button', { name: 'Sign in' }).click();
- await expect(page.getByText('Welcome to Twenty')).not.toBeVisible();
- await page.waitForTimeout(5000);
- await expect(page.getByText('Server’s on a coffee break')).not.toBeVisible({
- timeout: 5000,
- });
-});
+test.fixme(
+ 'Check if demo account is working properly @demo-only',
+ async ({ page }) => {
+ await page.goto('https://app.twenty-next.com/');
+ await page.getByRole('button', { name: 'Continue with Email' }).click();
+ await page.getByRole('button', { name: 'Continue', exact: true }).click();
+ await page.getByRole('button', { name: 'Sign in' }).click();
+ await expect(page.getByText('Welcome to Twenty')).not.toBeVisible();
+ await page.waitForTimeout(5000);
+ await expect(page.getByText('Server’s on a coffee break')).not.toBeVisible({
+ timeout: 5000,
+ });
+ },
+);
diff --git a/packages/twenty-front/package.json b/packages/twenty-front/package.json
index 7500dfcaa..a5b8be382 100644
--- a/packages/twenty-front/package.json
+++ b/packages/twenty-front/package.json
@@ -46,7 +46,7 @@
"@tiptap/extension-text": "^2.10.4",
"@tiptap/extension-text-style": "^2.10.4",
"@tiptap/react": "^2.10.4",
- "@xyflow/react": "^12.0.4",
+ "@xyflow/react": "^12.4.2",
"buffer": "^6.0.3",
"docx": "^9.1.0",
"file-saver": "^2.0.5",
diff --git a/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowDiagramCanvasBase.tsx b/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowDiagramCanvasBase.tsx
index 79854ba9e..ab90158d6 100644
--- a/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowDiagramCanvasBase.tsx
+++ b/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowDiagramCanvasBase.tsx
@@ -23,7 +23,6 @@ import {
ReactFlow,
applyEdgeChanges,
applyNodeChanges,
- getNodesBounds,
useReactFlow,
} from '@xyflow/react';
import '@xyflow/react/dist/style.css';
@@ -176,7 +175,7 @@ export const WorkflowDiagramCanvasBase = ({
const currentViewport = reactflow.getViewport();
- const flowBounds = getNodesBounds(reactflow.getNodes());
+ const flowBounds = reactflow.getNodesBounds(reactflow.getNodes());
let visibleRightDrawerWidth = 0;
if (rightDrawerState === 'normal') {
@@ -213,7 +212,7 @@ export const WorkflowDiagramCanvasBase = ({
throw new Error('Expect the container ref to be defined');
}
- const flowBounds = getNodesBounds(reactflow.getNodes());
+ const flowBounds = reactflow.getNodesBounds(reactflow.getNodes());
reactflow.setViewport({
x: containerRef.current.offsetWidth / 2 - flowBounds.width / 2,
diff --git a/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowDiagramCanvasEditableEffect.tsx b/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowDiagramCanvasEditableEffect.tsx
index 4855c93b6..0221a2728 100644
--- a/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowDiagramCanvasEditableEffect.tsx
+++ b/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowDiagramCanvasEditableEffect.tsx
@@ -3,7 +3,6 @@ import { useRightDrawer } from '@/ui/layout/right-drawer/hooks/useRightDrawer';
import { RightDrawerHotkeyScope } from '@/ui/layout/right-drawer/types/RightDrawerHotkeyScope';
import { RightDrawerPages } from '@/ui/layout/right-drawer/types/RightDrawerPages';
import { useSetHotkeyScope } from '@/ui/utilities/hotkey/hooks/useSetHotkeyScope';
-import { CREATE_STEP_STEP_ID } from '@/workflow/workflow-diagram/constants/CreateStepStepId';
import { EMPTY_TRIGGER_STEP_ID } from '@/workflow/workflow-diagram/constants/EmptyTriggerStepId';
import { useStartNodeCreation } from '@/workflow/workflow-diagram/hooks/useStartNodeCreation';
import { useTriggerNodeSelection } from '@/workflow/workflow-diagram/hooks/useTriggerNodeSelection';
@@ -13,6 +12,7 @@ import {
WorkflowDiagramStepNodeData,
} from '@/workflow/workflow-diagram/types/WorkflowDiagram';
import { getWorkflowNodeIconKey } from '@/workflow/workflow-diagram/utils/getWorkflowNodeIconKey';
+import { isCreateStepNode } from '@/workflow/workflow-diagram/utils/isCreateStepNode';
import { useLingui } from '@lingui/react/macro';
import { OnSelectionChangeParams, useOnSelectionChange } from '@xyflow/react';
import { useCallback } from 'react';
@@ -53,12 +53,7 @@ export const WorkflowDiagramCanvasEditableEffect = () => {
return;
}
- const isCreateStepNode = selectedNode.type === CREATE_STEP_STEP_ID;
- if (isCreateStepNode) {
- if (selectedNode.data.nodeType !== 'create-step') {
- throw new Error(t`Expected selected node to be a create step node.`);
- }
-
+ if (isCreateStepNode(selectedNode)) {
startNodeCreation(selectedNode.data.parentNodeId);
return;
diff --git a/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowDiagramEffect.tsx b/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowDiagramEffect.tsx
index 4dfbd4ad7..a85807c22 100644
--- a/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowDiagramEffect.tsx
+++ b/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowDiagramEffect.tsx
@@ -8,6 +8,7 @@ import { workflowDiagramState } from '@/workflow/workflow-diagram/states/workflo
import { addCreateStepNodes } from '@/workflow/workflow-diagram/utils/addCreateStepNodes';
import { getWorkflowVersionDiagram } from '@/workflow/workflow-diagram/utils/getWorkflowVersionDiagram';
+import { markLeafNodes } from '@/workflow/workflow-diagram/utils/markLeafNodes';
import { mergeWorkflowDiagrams } from '@/workflow/workflow-diagram/utils/mergeWorkflowDiagrams';
import { useEffect } from 'react';
import { useRecoilCallback, useSetRecoilState } from 'recoil';
@@ -28,8 +29,8 @@ export const WorkflowDiagramEffect = ({
workflowDiagramState,
);
- const nextWorkflowDiagram = addCreateStepNodes(
- getWorkflowVersionDiagram(currentVersion),
+ const nextWorkflowDiagram = markLeafNodes(
+ addCreateStepNodes(getWorkflowVersionDiagram(currentVersion)),
);
let mergedWorkflowDiagram = nextWorkflowDiagram;
diff --git a/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowDiagramEmptyTrigger.tsx b/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowDiagramEmptyTrigger.tsx
index dd1528b8f..da814e224 100644
--- a/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowDiagramEmptyTrigger.tsx
+++ b/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowDiagramEmptyTrigger.tsx
@@ -1,4 +1,5 @@
import { WorkflowDiagramStepNodeBase } from '@/workflow/workflow-diagram/components/WorkflowDiagramStepNodeBase';
+import { WorkflowDiagramEmptyTriggerNodeData } from '@/workflow/workflow-diagram/types/WorkflowDiagram';
import styled from '@emotion/styled';
const StyledStepNodeLabelIconContainer = styled.div`
@@ -10,13 +11,18 @@ const StyledStepNodeLabelIconContainer = styled.div`
padding: ${({ theme }) => theme.spacing(3)};
`;
-export const WorkflowDiagramEmptyTrigger = () => {
+export const WorkflowDiagramEmptyTrigger = ({
+ data,
+}: {
+ data: WorkflowDiagramEmptyTriggerNodeData;
+}) => {
return (
}
+ isLeafNode={data.isLeafNode}
/>
);
};
diff --git a/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowDiagramStepNodeBase.tsx b/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowDiagramStepNodeBase.tsx
index 7c0b651d4..8590d82cd 100644
--- a/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowDiagramStepNodeBase.tsx
+++ b/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowDiagramStepNodeBase.tsx
@@ -182,12 +182,14 @@ export const WorkflowDiagramStepNodeBase = ({
variant,
Icon,
RightFloatingElement,
+ isLeafNode,
}: {
nodeType: WorkflowDiagramStepNodeData['nodeType'];
name: string;
variant: WorkflowDiagramNodeVariant;
Icon?: React.ReactNode;
RightFloatingElement?: React.ReactNode;
+ isLeafNode: boolean;
}) => {
return (
@@ -213,7 +215,9 @@ export const WorkflowDiagramStepNodeBase = ({
) : null}
-
+ {!isLeafNode && (
+
+ )}
);
};
diff --git a/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowDiagramStepNodeEditableContent.tsx b/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowDiagramStepNodeEditableContent.tsx
index ff03ef61d..4b33887c1 100644
--- a/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowDiagramStepNodeEditableContent.tsx
+++ b/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowDiagramStepNodeEditableContent.tsx
@@ -30,6 +30,7 @@ export const WorkflowDiagramStepNodeEditableContent = ({
/>
) : undefined
}
+ isLeafNode={data.isLeafNode}
/>
);
};
diff --git a/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowDiagramStepNodeReadonly.tsx b/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowDiagramStepNodeReadonly.tsx
index 48bd8a9c1..7811bb3aa 100644
--- a/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowDiagramStepNodeReadonly.tsx
+++ b/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowDiagramStepNodeReadonly.tsx
@@ -13,6 +13,7 @@ export const WorkflowDiagramStepNodeReadonly = ({
variant="default"
nodeType={data.nodeType}
Icon={}
+ isLeafNode={data.isLeafNode}
/>
);
};
diff --git a/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowVersionVisualizerEffect.tsx b/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowVersionVisualizerEffect.tsx
index 95da1b187..3d5a3e4c1 100644
--- a/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowVersionVisualizerEffect.tsx
+++ b/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowVersionVisualizerEffect.tsx
@@ -2,6 +2,7 @@ import { useWorkflowVersion } from '@/workflow/hooks/useWorkflowVersion';
import { workflowVersionIdState } from '@/workflow/states/workflowVersionIdState';
import { workflowDiagramState } from '@/workflow/workflow-diagram/states/workflowDiagramState';
import { getWorkflowVersionDiagram } from '@/workflow/workflow-diagram/utils/getWorkflowVersionDiagram';
+import { markLeafNodes } from '@/workflow/workflow-diagram/utils/markLeafNodes';
import { useEffect } from 'react';
import { useSetRecoilState } from 'recoil';
import { isDefined } from 'twenty-shared';
@@ -27,7 +28,9 @@ export const WorkflowVersionVisualizerEffect = ({
return;
}
- const nextWorkflowDiagram = getWorkflowVersionDiagram(workflowVersion);
+ const nextWorkflowDiagram = markLeafNodes(
+ getWorkflowVersionDiagram(workflowVersion),
+ );
setWorkflowDiagram(nextWorkflowDiagram);
}, [setWorkflowDiagram, workflowVersion]);
diff --git a/packages/twenty-front/src/modules/workflow/workflow-diagram/components/__stories__/WorkflowDiagramEmptyTrigger.stories.tsx b/packages/twenty-front/src/modules/workflow/workflow-diagram/components/__stories__/WorkflowDiagramEmptyTrigger.stories.tsx
index e7b5aac82..eee78f2b3 100644
--- a/packages/twenty-front/src/modules/workflow/workflow-diagram/components/__stories__/WorkflowDiagramEmptyTrigger.stories.tsx
+++ b/packages/twenty-front/src/modules/workflow/workflow-diagram/components/__stories__/WorkflowDiagramEmptyTrigger.stories.tsx
@@ -1,13 +1,19 @@
import { Meta, StoryObj } from '@storybook/react';
import { ComponentDecorator } from 'twenty-ui';
-import { ReactFlowProvider } from '@xyflow/react';
import '@xyflow/react/dist/style.css';
+import { ReactflowDecorator } from '~/testing/decorators/ReactflowDecorator';
import { WorkflowDiagramEmptyTrigger } from '../WorkflowDiagramEmptyTrigger';
const meta: Meta = {
title: 'Modules/Workflow/WorkflowDiagramEmptyTrigger',
component: WorkflowDiagramEmptyTrigger,
+ args: {
+ data: {
+ nodeType: 'empty-trigger',
+ isLeafNode: true,
+ },
+ },
};
export default meta;
@@ -16,12 +22,11 @@ type Story = StoryObj;
export const Default: Story = {
decorators: [
(Story) => (
-
-
-
-
-
+
+
+
),
+ ReactflowDecorator,
ComponentDecorator,
],
};
@@ -33,11 +38,25 @@ export const Selected: Story = {
),
- (Story) => (
-
-
-
- ),
+ ReactflowDecorator,
ComponentDecorator,
],
};
+
+export const IsNotLeafNode: Story = {
+ decorators: [
+ (Story) => (
+
+
+
+ ),
+ ComponentDecorator,
+ ReactflowDecorator,
+ ],
+ args: {
+ data: {
+ nodeType: 'empty-trigger',
+ isLeafNode: false,
+ },
+ },
+};
diff --git a/packages/twenty-front/src/modules/workflow/workflow-diagram/components/__stories__/WorkflowDiagramStepNodeEditableContent.stories.tsx b/packages/twenty-front/src/modules/workflow/workflow-diagram/components/__stories__/WorkflowDiagramStepNodeEditableContent.stories.tsx
index 4da876924..7a1b1f6ce 100644
--- a/packages/twenty-front/src/modules/workflow/workflow-diagram/components/__stories__/WorkflowDiagramStepNodeEditableContent.stories.tsx
+++ b/packages/twenty-front/src/modules/workflow/workflow-diagram/components/__stories__/WorkflowDiagramStepNodeEditableContent.stories.tsx
@@ -23,6 +23,9 @@ const Wrapper = (_props: WrapperProps) => {
const meta: Meta = {
title: 'Modules/Workflow/WorkflowDiagramStepNodeEditableContent',
component: WorkflowDiagramStepNodeEditableContent,
+ parameters: {
+ msw: graphqlMocks,
+ },
};
export default meta;
@@ -34,29 +37,44 @@ const ALL_STEPS = [
nodeType: 'trigger',
triggerType: 'DATABASE_EVENT',
name: 'Record is Created',
+ isLeafNode: true,
+ },
+ {
+ nodeType: 'trigger',
+ triggerType: 'MANUAL',
+ name: 'Manual',
+ isLeafNode: true,
},
- { nodeType: 'trigger', triggerType: 'MANUAL', name: 'Manual' },
{
nodeType: 'action',
actionType: 'CREATE_RECORD',
name: 'Create Record',
+ isLeafNode: true,
},
{
nodeType: 'action',
actionType: 'UPDATE_RECORD',
name: 'Update Record',
+ isLeafNode: true,
},
{
nodeType: 'action',
actionType: 'DELETE_RECORD',
name: 'Delete Record',
+ isLeafNode: true,
},
{
nodeType: 'action',
actionType: 'SEND_EMAIL',
name: 'Send Email',
+ isLeafNode: true,
+ },
+ {
+ nodeType: 'action',
+ actionType: 'CODE',
+ name: 'Code',
+ isLeafNode: true,
},
- { nodeType: 'action', actionType: 'CODE', name: 'Code' },
] satisfies WorkflowDiagramStepNodeData[];
export const Catalog: CatalogStory = {
@@ -64,7 +82,6 @@ export const Catalog: CatalogStory = {
onDelete: fn(),
},
parameters: {
- msw: graphqlMocks,
pseudo: { hover: ['.hover'] },
catalog: {
options: {
@@ -112,3 +129,22 @@ export const Catalog: CatalogStory = {
ReactflowDecorator,
],
};
+
+export const IsNotLeafNode: Story = {
+ args: {
+ data: {
+ ...ALL_STEPS[0],
+ isLeafNode: false,
+ },
+ state: 'default',
+ variant: 'default',
+ },
+ decorators: [
+ (Story) => (
+
+
+
+ ),
+ ReactflowDecorator,
+ ],
+};
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 69b1f318b..2d909f66d 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
@@ -18,21 +18,30 @@ export type WorkflowDiagramStepNodeData =
triggerType: WorkflowTriggerType;
name: string;
icon?: string;
+ isLeafNode: boolean;
}
| {
nodeType: 'action';
actionType: WorkflowActionType;
name: string;
+ isLeafNode: boolean;
};
export type WorkflowDiagramCreateStepNodeData = {
nodeType: 'create-step';
parentNodeId: string;
+ isLeafNode?: never;
+};
+
+export type WorkflowDiagramEmptyTriggerNodeData = {
+ nodeType: 'empty-trigger';
+ isLeafNode: boolean;
};
export type WorkflowDiagramNodeData =
| WorkflowDiagramStepNodeData
- | WorkflowDiagramCreateStepNodeData;
+ | WorkflowDiagramCreateStepNodeData
+ | WorkflowDiagramEmptyTriggerNodeData;
export type WorkflowDiagramNodeType =
| 'default'
diff --git a/packages/twenty-front/src/modules/workflow/workflow-diagram/utils/__tests__/generateWorkflowDiagram.test.ts b/packages/twenty-front/src/modules/workflow/workflow-diagram/utils/__tests__/generateWorkflowDiagram.test.ts
index 1ec663b34..c6cce3d8b 100644
--- a/packages/twenty-front/src/modules/workflow/workflow-diagram/utils/__tests__/generateWorkflowDiagram.test.ts
+++ b/packages/twenty-front/src/modules/workflow/workflow-diagram/utils/__tests__/generateWorkflowDiagram.test.ts
@@ -21,6 +21,7 @@ describe('generateWorkflowDiagram', () => {
expect(result.nodes[0]).toMatchObject({
data: {
nodeType: 'trigger',
+ isLeafNode: false,
},
});
});
@@ -87,6 +88,7 @@ describe('generateWorkflowDiagram', () => {
nodeType: 'action',
actionType: 'CODE',
name: step.name,
+ isLeafNode: false,
});
}
});
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 1bb5185e3..925bc753a 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
@@ -1,10 +1,20 @@
+import { getUuidV4Mock } from '~/testing/utils/getUuidV4Mock';
import { getWorkflowVersionDiagram } from '../getWorkflowVersionDiagram';
+jest.mock('uuid', () => ({
+ v4: getUuidV4Mock(),
+}));
+
describe('getWorkflowVersionDiagram', () => {
it('returns an empty diagram if the provided workflow version', () => {
const result = getWorkflowVersionDiagram(undefined);
- expect(result).toEqual({ nodes: [], edges: [] });
+ expect(result).toMatchInlineSnapshot(`
+{
+ "edges": [],
+ "nodes": [],
+}
+`);
});
it('returns a diagram with an empty-trigger node if the provided workflow version has no trigger', () => {
@@ -20,17 +30,25 @@ describe('getWorkflowVersionDiagram', () => {
workflowId: '',
});
- expect(result).toEqual({
- nodes: [
- {
- data: {},
- id: 'trigger',
- position: { x: 0, y: 0 },
- type: 'empty-trigger',
- },
- ],
- edges: [],
- });
+ expect(result).toMatchInlineSnapshot(`
+{
+ "edges": [],
+ "nodes": [
+ {
+ "data": {
+ "isLeafNode": false,
+ "nodeType": "empty-trigger",
+ },
+ "id": "trigger",
+ "position": {
+ "x": 0,
+ "y": 0,
+ },
+ "type": "empty-trigger",
+ },
+ ],
+}
+`);
});
it('returns a diagram with only a trigger node if the provided workflow version has no steps', () => {
@@ -50,21 +68,27 @@ describe('getWorkflowVersionDiagram', () => {
workflowId: '',
});
- expect(result).toEqual({
- nodes: [
- {
- data: {
- name: 'Record is created',
- nodeType: 'trigger',
- triggerType: 'DATABASE_EVENT',
- icon: 'IconPlus',
- },
- id: 'trigger',
- position: { x: 0, y: 0 },
- },
- ],
- edges: [],
- });
+ expect(result).toMatchInlineSnapshot(`
+{
+ "edges": [],
+ "nodes": [
+ {
+ "data": {
+ "icon": "IconPlus",
+ "isLeafNode": false,
+ "name": "Record is created",
+ "nodeType": "trigger",
+ "triggerType": "DATABASE_EVENT",
+ },
+ "id": "trigger",
+ "position": {
+ "x": 0,
+ "y": 0,
+ },
+ },
+ ],
+}
+`);
});
it('returns the diagram for the last version', () => {
@@ -103,8 +127,48 @@ describe('getWorkflowVersionDiagram', () => {
workflowId: '',
});
- // Corresponds to the trigger + 1 step
- expect(result.nodes).toHaveLength(2);
- expect(result.edges).toHaveLength(1);
+ expect(result).toMatchInlineSnapshot(`
+{
+ "edges": [
+ {
+ "deletable": false,
+ "id": "8f3b2121-f194-4ba4-9fbf-0",
+ "markerEnd": "arrow-rounded",
+ "selectable": false,
+ "source": "trigger",
+ "target": "step-1",
+ },
+ ],
+ "nodes": [
+ {
+ "data": {
+ "icon": "IconPlus",
+ "isLeafNode": false,
+ "name": "Company created",
+ "nodeType": "trigger",
+ "triggerType": "DATABASE_EVENT",
+ },
+ "id": "trigger",
+ "position": {
+ "x": 0,
+ "y": 0,
+ },
+ },
+ {
+ "data": {
+ "actionType": "CODE",
+ "isLeafNode": false,
+ "name": "",
+ "nodeType": "action",
+ },
+ "id": "step-1",
+ "position": {
+ "x": 150,
+ "y": 100,
+ },
+ },
+ ],
+}
+`);
});
});
diff --git a/packages/twenty-front/src/modules/workflow/workflow-diagram/utils/__tests__/markLeafNodes.test.ts b/packages/twenty-front/src/modules/workflow/workflow-diagram/utils/__tests__/markLeafNodes.test.ts
new file mode 100644
index 000000000..ae5034609
--- /dev/null
+++ b/packages/twenty-front/src/modules/workflow/workflow-diagram/utils/__tests__/markLeafNodes.test.ts
@@ -0,0 +1,69 @@
+import { WorkflowStep, WorkflowTrigger } from '@/workflow/types/Workflow';
+import { generateWorkflowDiagram } from '@/workflow/workflow-diagram/utils/generateWorkflowDiagram';
+import { markLeafNodes } from '../markLeafNodes';
+
+describe('markLeafNodes', () => {
+ const createTrigger = (): WorkflowTrigger => ({
+ name: 'Company created',
+ type: 'DATABASE_EVENT',
+ settings: {
+ eventName: 'company.created',
+ outputSchema: {},
+ },
+ });
+
+ const createStep = (id: string): WorkflowStep => ({
+ id,
+ name: `Step ${id}`,
+ type: 'CODE',
+ valid: true,
+ settings: {
+ errorHandlingOptions: {
+ retryOnFailure: { value: true },
+ continueOnFailure: { value: false },
+ },
+ input: {
+ serverlessFunctionId: 'a5434be2-c10b-465c-acec-46492782a997',
+ serverlessFunctionVersion: '1',
+ serverlessFunctionInput: {},
+ },
+ outputSchema: {},
+ },
+ });
+
+ it('handles empty workflow with only trigger', () => {
+ const trigger = createTrigger();
+ const steps: WorkflowStep[] = [];
+
+ const diagram = generateWorkflowDiagram({ trigger, steps });
+ const diagramWithLeafNodes = markLeafNodes(diagram);
+
+ expect(diagramWithLeafNodes.nodes).toHaveLength(1);
+ expect(diagramWithLeafNodes.nodes[0].data.isLeafNode).toBe(true);
+ });
+
+ it('handles workflow with single step', () => {
+ const trigger = createTrigger();
+ const steps = [createStep('step1')];
+
+ const diagram = generateWorkflowDiagram({ trigger, steps });
+ const diagramWithLeafNodes = markLeafNodes(diagram);
+
+ expect(diagramWithLeafNodes.nodes).toHaveLength(2);
+ expect(diagramWithLeafNodes.nodes[0].data.isLeafNode).toBe(false);
+ expect(diagramWithLeafNodes.nodes[1].data.isLeafNode).toBe(true);
+ });
+
+ it('handles workflow with two steps', () => {
+ const trigger = createTrigger();
+ const steps = [createStep('step1'), createStep('step2')];
+
+ const diagram = generateWorkflowDiagram({ trigger, steps });
+ const diagramWithLeafNodes = markLeafNodes(diagram);
+
+ expect(diagramWithLeafNodes.nodes).toHaveLength(3);
+ expect(diagramWithLeafNodes.nodes[0].data.isLeafNode).toBe(false);
+ expect(diagramWithLeafNodes.nodes[1].data.isLeafNode).toBe(false);
+ expect(diagramWithLeafNodes.nodes[2].data.isLeafNode).toBe(true);
+ });
+});
diff --git a/packages/twenty-front/src/modules/workflow/workflow-diagram/utils/__tests__/mergeWorkflowDiagrams.test.ts b/packages/twenty-front/src/modules/workflow/workflow-diagram/utils/__tests__/mergeWorkflowDiagrams.test.ts
index a1f39ae5d..ab3da4c88 100644
--- a/packages/twenty-front/src/modules/workflow/workflow-diagram/utils/__tests__/mergeWorkflowDiagrams.test.ts
+++ b/packages/twenty-front/src/modules/workflow/workflow-diagram/utils/__tests__/mergeWorkflowDiagrams.test.ts
@@ -5,7 +5,12 @@ it('Preserves the properties defined in the previous version but not in the next
const previousDiagram: WorkflowDiagram = {
nodes: [
{
- data: { nodeType: 'action', name: '', actionType: 'CODE' },
+ data: {
+ nodeType: 'action',
+ name: '',
+ actionType: 'CODE',
+ isLeafNode: true,
+ },
id: '1',
position: { x: 0, y: 0 },
selected: true,
@@ -16,7 +21,12 @@ it('Preserves the properties defined in the previous version but not in the next
const nextDiagram: WorkflowDiagram = {
nodes: [
{
- data: { nodeType: 'action', name: '', actionType: 'CODE' },
+ data: {
+ nodeType: 'action',
+ name: '',
+ actionType: 'CODE',
+ isLeafNode: true,
+ },
id: '1',
position: { x: 0, y: 0 },
},
@@ -24,24 +34,40 @@ it('Preserves the properties defined in the previous version but not in the next
edges: [],
};
- expect(mergeWorkflowDiagrams(previousDiagram, nextDiagram)).toEqual({
- nodes: [
- {
- data: { nodeType: 'action', name: '', actionType: 'CODE' },
- id: '1',
- position: { x: 0, y: 0 },
- selected: true,
+ expect(mergeWorkflowDiagrams(previousDiagram, nextDiagram))
+ .toMatchInlineSnapshot(`
+{
+ "edges": [],
+ "nodes": [
+ {
+ "data": {
+ "actionType": "CODE",
+ "isLeafNode": true,
+ "name": "",
+ "nodeType": "action",
},
- ],
- edges: [],
- });
+ "id": "1",
+ "position": {
+ "x": 0,
+ "y": 0,
+ },
+ "selected": true,
+ },
+ ],
+}
+`);
});
it('Replaces duplicated properties with the next value', () => {
const previousDiagram: WorkflowDiagram = {
nodes: [
{
- data: { nodeType: 'action', name: '', actionType: 'CODE' },
+ data: {
+ nodeType: 'action',
+ name: '',
+ actionType: 'CODE',
+ isLeafNode: true,
+ },
id: '1',
position: { x: 0, y: 0 },
},
@@ -51,7 +77,12 @@ it('Replaces duplicated properties with the next value', () => {
const nextDiagram: WorkflowDiagram = {
nodes: [
{
- data: { nodeType: 'action', name: '2', actionType: 'CODE' },
+ data: {
+ nodeType: 'action',
+ name: '2',
+ actionType: 'CODE',
+ isLeafNode: false,
+ },
id: '1',
position: { x: 0, y: 0 },
},
@@ -59,14 +90,26 @@ it('Replaces duplicated properties with the next value', () => {
edges: [],
};
- expect(mergeWorkflowDiagrams(previousDiagram, nextDiagram)).toEqual({
- nodes: [
- {
- data: { nodeType: 'action', name: '2', actionType: 'CODE' },
- id: '1',
- position: { x: 0, y: 0 },
+ expect(mergeWorkflowDiagrams(previousDiagram, nextDiagram))
+ .toMatchInlineSnapshot(`
+{
+ "edges": [],
+ "nodes": [
+ {
+ "data": {
+ "actionType": "CODE",
+ "isLeafNode": false,
+ "name": "2",
+ "nodeType": "action",
},
- ],
- edges: [],
- });
+ "id": "1",
+ "position": {
+ "x": 0,
+ "y": 0,
+ },
+ "selected": undefined,
+ },
+ ],
+}
+`);
});
diff --git a/packages/twenty-front/src/modules/workflow/workflow-diagram/utils/generateWorkflowDiagram.ts b/packages/twenty-front/src/modules/workflow/workflow-diagram/utils/generateWorkflowDiagram.ts
index c60eeb9f3..906d8b0b9 100644
--- a/packages/twenty-front/src/modules/workflow/workflow-diagram/utils/generateWorkflowDiagram.ts
+++ b/packages/twenty-front/src/modules/workflow/workflow-diagram/utils/generateWorkflowDiagram.ts
@@ -5,7 +5,9 @@ import { WORKFLOW_VISUALIZER_EDGE_DEFAULT_CONFIGURATION } from '@/workflow/workf
import {
WorkflowDiagram,
WorkflowDiagramEdge,
+ WorkflowDiagramEmptyTriggerNodeData,
WorkflowDiagramNode,
+ WorkflowDiagramStepNodeData,
} from '@/workflow/workflow-diagram/types/WorkflowDiagram';
import { DATABASE_TRIGGER_TYPES } from '@/workflow/workflow-trigger/constants/DatabaseTriggerTypes';
@@ -38,7 +40,8 @@ export const generateWorkflowDiagram = ({
nodeType: 'action',
actionType: step.type,
name: step.name,
- },
+ isLeafNode: false,
+ } satisfies WorkflowDiagramStepNodeData,
position: {
x: xPos,
y: yPos,
@@ -102,7 +105,8 @@ export const generateWorkflowDiagram = ({
triggerType: trigger.type,
name: isDefined(trigger.name) ? trigger.name : triggerDefaultLabel,
icon: triggerIcon,
- },
+ isLeafNode: false,
+ } satisfies WorkflowDiagramStepNodeData,
position: {
x: 0,
y: 0,
@@ -112,7 +116,10 @@ export const generateWorkflowDiagram = ({
nodes.push({
id: triggerNodeId,
type: 'empty-trigger',
- data: {} as any,
+ data: {
+ nodeType: 'empty-trigger',
+ isLeafNode: false,
+ } satisfies WorkflowDiagramEmptyTriggerNodeData,
position: {
x: 0,
y: 0,
diff --git a/packages/twenty-front/src/modules/workflow/workflow-diagram/utils/isCreateStepNode.ts b/packages/twenty-front/src/modules/workflow/workflow-diagram/utils/isCreateStepNode.ts
new file mode 100644
index 000000000..aba8e17f8
--- /dev/null
+++ b/packages/twenty-front/src/modules/workflow/workflow-diagram/utils/isCreateStepNode.ts
@@ -0,0 +1,15 @@
+import { CREATE_STEP_STEP_ID } from '@/workflow/workflow-diagram/constants/CreateStepStepId';
+import {
+ WorkflowDiagramCreateStepNodeData,
+ WorkflowDiagramNode,
+} from '@/workflow/workflow-diagram/types/WorkflowDiagram';
+
+export const isCreateStepNode = (
+ node: WorkflowDiagramNode,
+): node is WorkflowDiagramNode & {
+ data: WorkflowDiagramCreateStepNodeData;
+} => {
+ return (
+ node.type === CREATE_STEP_STEP_ID && node.data.nodeType === 'create-step'
+ );
+};
diff --git a/packages/twenty-front/src/modules/workflow/workflow-diagram/utils/markLeafNodes.ts b/packages/twenty-front/src/modules/workflow/workflow-diagram/utils/markLeafNodes.ts
new file mode 100644
index 000000000..3582e1ba3
--- /dev/null
+++ b/packages/twenty-front/src/modules/workflow/workflow-diagram/utils/markLeafNodes.ts
@@ -0,0 +1,31 @@
+import {
+ WorkflowDiagram,
+ WorkflowDiagramNode,
+} from '@/workflow/workflow-diagram/types/WorkflowDiagram';
+import { isCreateStepNode } from '@/workflow/workflow-diagram/utils/isCreateStepNode';
+
+export const markLeafNodes = ({
+ nodes,
+ edges,
+}: WorkflowDiagram): WorkflowDiagram => {
+ const sourceNodeIds = new Set(edges.map((edge) => edge.source));
+
+ const updatedNodes = nodes.map((node) => {
+ if (isCreateStepNode(node)) {
+ return node;
+ }
+
+ return {
+ ...node,
+ data: {
+ ...node.data,
+ isLeafNode: !sourceNodeIds.has(node.id),
+ },
+ };
+ });
+
+ return {
+ nodes: updatedNodes as WorkflowDiagramNode[],
+ edges,
+ };
+};
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 2da43ab34..dfa93c42b 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
@@ -59,6 +59,7 @@ export const useCreateStep = ({
nodeType: 'action',
actionType: createdStep.type as WorkflowStepType,
name: createdStep.name,
+ isLeafNode: false,
});
openRightDrawer(RightDrawerPages.WorkflowStepEdit, {
diff --git a/packages/twenty-front/src/testing/utils/getUuidV4Mock.ts b/packages/twenty-front/src/testing/utils/getUuidV4Mock.ts
new file mode 100644
index 000000000..0f3b79663
--- /dev/null
+++ b/packages/twenty-front/src/testing/utils/getUuidV4Mock.ts
@@ -0,0 +1,9 @@
+const baseUuid = '8f3b2121-f194-4ba4-9fbf-';
+
+export const getUuidV4Mock = () => {
+ let id = 0;
+
+ return () => {
+ return baseUuid + id++;
+ };
+};
diff --git a/yarn.lock b/yarn.lock
index a3c046820..7a19370cf 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -18919,37 +18919,23 @@ __metadata:
languageName: node
linkType: hard
-"@xyflow/react@npm:^12.0.4":
- version: 12.0.4
- resolution: "@xyflow/react@npm:12.0.4"
+"@xyflow/react@npm:^12.4.2":
+ version: 12.4.2
+ resolution: "@xyflow/react@npm:12.4.2"
dependencies:
- "@xyflow/system": "npm:0.0.37"
+ "@xyflow/system": "npm:0.0.50"
classcat: "npm:^5.0.3"
zustand: "npm:^4.4.0"
peerDependencies:
react: ">=17"
react-dom: ">=17"
- checksum: 10c0/57b04024c3cca1b5d19b5625b92a5ca5015870a5b6adf2ab2c0bcfa701f93929805777ad081e7142b9c94846ad83d65abb65041b50134515b135b6514d74766e
+ checksum: 10c0/7f58fd5fa7d9a04645228ad867273c660cc4ca4b77f8dc045c4d2dd52dec2ce31d5a7d92290ec54ea46aaf2e32e4fbf90f81c07cdd4da5ee8a64f06bea6ab373
languageName: node
linkType: hard
-"@xyflow/react@npm:^12.3.5":
- version: 12.3.5
- resolution: "@xyflow/react@npm:12.3.5"
- dependencies:
- "@xyflow/system": "npm:0.0.46"
- classcat: "npm:^5.0.3"
- zustand: "npm:^4.4.0"
- peerDependencies:
- react: ">=17"
- react-dom: ">=17"
- checksum: 10c0/f4eb2f8ed31454aa2bbc7fef3b3e9592093cbf238ca7ba572d002a0bd5fac267d488b6d560d173ee610c83e02ca0e9505c35083bdedc9890c1a65f52297f8c1c
- languageName: node
- linkType: hard
-
-"@xyflow/system@npm:0.0.37":
- version: 0.0.37
- resolution: "@xyflow/system@npm:0.0.37"
+"@xyflow/system@npm:0.0.50":
+ version: 0.0.50
+ resolution: "@xyflow/system@npm:0.0.50"
dependencies:
"@types/d3-drag": "npm:^3.0.7"
"@types/d3-selection": "npm:^3.0.10"
@@ -18958,22 +18944,7 @@ __metadata:
d3-drag: "npm:^3.0.0"
d3-selection: "npm:^3.0.0"
d3-zoom: "npm:^3.0.0"
- checksum: 10c0/60b2de70a53dc3f2b691d837f2adcd2324f2e3e19258d6928e58578ad896a7f9fa7dd20938b224e7054284542135e0d7519ab34c012d69a8ed0e15ecf452d1ee
- languageName: node
- linkType: hard
-
-"@xyflow/system@npm:0.0.46":
- version: 0.0.46
- resolution: "@xyflow/system@npm:0.0.46"
- dependencies:
- "@types/d3-drag": "npm:^3.0.7"
- "@types/d3-selection": "npm:^3.0.10"
- "@types/d3-transition": "npm:^3.0.8"
- "@types/d3-zoom": "npm:^3.0.8"
- d3-drag: "npm:^3.0.0"
- d3-selection: "npm:^3.0.0"
- d3-zoom: "npm:^3.0.0"
- checksum: 10c0/973886c03a389e96d504ef6e8ff350949688d7a82f159549ac2a38f7f11ebed2ce5b65b52c70bd7d9f344247c913dc751c79b737953d8759d7d13e98a5ee512d
+ checksum: 10c0/7a7e45340efb7e59f898eed726a1f3323857bdeb5b700eb3f2d9338f0bbddccb75c74ddecae15b244fbefe3d5a45d58546e7768730797d39f8219181c8a65753
languageName: node
linkType: hard
@@ -45894,7 +45865,7 @@ __metadata:
"@tiptap/extension-text-style": "npm:^2.10.4"
"@tiptap/react": "npm:^2.10.4"
"@types/file-saver": "npm:^2"
- "@xyflow/react": "npm:^12.0.4"
+ "@xyflow/react": "npm:^12.4.2"
buffer: "npm:^6.0.3"
docx: "npm:^9.1.0"
file-saver: "npm:^2.0.5"
@@ -46175,7 +46146,7 @@ __metadata:
"@vitejs/plugin-react-swc": "npm:^3.5.0"
"@vitest/ui": "npm:1.4.0"
"@wyw-in-js/vite": "npm:^0.5.3"
- "@xyflow/react": "npm:^12.3.5"
+ "@xyflow/react": "npm:^12.4.2"
add: "npm:^2.0.6"
addressparser: "npm:^1.0.1"
afterframe: "npm:^1.0.2"