diff --git a/packages/twenty-front/setupTests.ts b/packages/twenty-front/setupTests.ts
index 8f2609b7b3..e5aa41c46c 100644
--- a/packages/twenty-front/setupTests.ts
+++ b/packages/twenty-front/setupTests.ts
@@ -3,3 +3,14 @@
// expect(element).toHaveTextContent(/react/i)
// learn more: https://github.com/testing-library/jest-dom
import '@testing-library/jest-dom';
+
+/**
+ * The structuredClone global function is not available in jsdom, it needs to be mocked for now.
+ *
+ * The most naive way to mock structuredClone is to use JSON.stringify and JSON.parse. This works
+ * for arguments with simple types like primitives, arrays and objects, but doesn't work with functions,
+ * Map, Set, etc.
+ */
+global.structuredClone = (val) => {
+ return JSON.parse(JSON.stringify(val));
+};
diff --git a/packages/twenty-front/src/hooks/__tests__/usePageChangeEffectNavigateLocation.test.ts b/packages/twenty-front/src/hooks/__tests__/usePageChangeEffectNavigateLocation.test.ts
index 95c2a58b79..bde6abe9c7 100644
--- a/packages/twenty-front/src/hooks/__tests__/usePageChangeEffectNavigateLocation.test.ts
+++ b/packages/twenty-front/src/hooks/__tests__/usePageChangeEffectNavigateLocation.test.ts
@@ -244,6 +244,17 @@ const testCases = [
{ loc: AppPath.Impersonate, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.InviteTeam, res: AppPath.InviteTeam },
{ loc: AppPath.Impersonate, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.Completed, res: undefined },
+ { loc: AppPath.WorkflowShowPage, isLoggedIn: true, subscriptionStatus: undefined, onboardingStatus: OnboardingStatus.PlanRequired, res: AppPath.PlanRequired },
+ { loc: AppPath.WorkflowShowPage, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.Canceled, onboardingStatus: OnboardingStatus.Completed, res: '/settings/billing' },
+ { loc: AppPath.WorkflowShowPage, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.Unpaid, onboardingStatus: OnboardingStatus.Completed, res: '/settings/billing' },
+ { loc: AppPath.WorkflowShowPage, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.PastDue, onboardingStatus: OnboardingStatus.Completed, res: undefined },
+ { loc: AppPath.WorkflowShowPage, isLoggedIn: false, subscriptionStatus: undefined, onboardingStatus: undefined, res: AppPath.SignInUp },
+ { loc: AppPath.WorkflowShowPage, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.WorkspaceActivation, res: AppPath.CreateWorkspace },
+ { loc: AppPath.WorkflowShowPage, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.ProfileCreation, res: AppPath.CreateProfile },
+ { loc: AppPath.WorkflowShowPage, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.SyncEmail, res: AppPath.SyncEmails },
+ { loc: AppPath.WorkflowShowPage, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.InviteTeam, res: AppPath.InviteTeam },
+ { loc: AppPath.WorkflowShowPage, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.Completed, res: undefined },
+
{ loc: AppPath.Authorize, isLoggedIn: true, subscriptionStatus: undefined, onboardingStatus: OnboardingStatus.PlanRequired, res: AppPath.PlanRequired },
{ loc: AppPath.Authorize, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.Canceled, onboardingStatus: OnboardingStatus.Completed, res: '/settings/billing' },
{ loc: AppPath.Authorize, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.Unpaid, onboardingStatus: OnboardingStatus.Completed, res: '/settings/billing' },
diff --git a/packages/twenty-front/src/modules/object-metadata/types/CoreObjectNameSingular.ts b/packages/twenty-front/src/modules/object-metadata/types/CoreObjectNameSingular.ts
index 6e0d13f48d..dd496e70d3 100644
--- a/packages/twenty-front/src/modules/object-metadata/types/CoreObjectNameSingular.ts
+++ b/packages/twenty-front/src/modules/object-metadata/types/CoreObjectNameSingular.ts
@@ -30,4 +30,5 @@ export enum CoreObjectNameSingular {
MessageThreadSubscriber = 'messageThreadSubscriber',
Workflow = 'workflow',
MessageChannelMessageAssociation = 'messageChannelMessageAssociation',
+ WorkflowVersion = 'workflowVersion',
}
diff --git a/packages/twenty-front/src/modules/ui/layout/hooks/__tests__/useShowAuthModal.test.tsx b/packages/twenty-front/src/modules/ui/layout/hooks/__tests__/useShowAuthModal.test.tsx
index ee0a465904..e81f8701e8 100644
--- a/packages/twenty-front/src/modules/ui/layout/hooks/__tests__/useShowAuthModal.test.tsx
+++ b/packages/twenty-front/src/modules/ui/layout/hooks/__tests__/useShowAuthModal.test.tsx
@@ -254,6 +254,17 @@ const testCases = [
{ loc: AppPath.Impersonate, isLogged: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.InviteTeam, res: true },
{ loc: AppPath.Impersonate, isLogged: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.Completed, res: false },
+ { loc: AppPath.WorkflowShowPage, isLogged: true, subscriptionStatus: undefined, onboardingStatus: OnboardingStatus.PlanRequired, res: true },
+ { loc: AppPath.WorkflowShowPage, isLogged: true, subscriptionStatus: SubscriptionStatus.Canceled, onboardingStatus: OnboardingStatus.Completed, res: false },
+ { loc: AppPath.WorkflowShowPage, isLogged: true, subscriptionStatus: SubscriptionStatus.Unpaid, onboardingStatus: OnboardingStatus.Completed, res: false },
+ { loc: AppPath.WorkflowShowPage, isLogged: true, subscriptionStatus: SubscriptionStatus.PastDue, onboardingStatus: OnboardingStatus.Completed, res: false },
+ { loc: AppPath.WorkflowShowPage, isLogged: false, subscriptionStatus: undefined, onboardingStatus: undefined, res: true },
+ { loc: AppPath.WorkflowShowPage, isLogged: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.WorkspaceActivation, res: true },
+ { loc: AppPath.WorkflowShowPage, isLogged: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.ProfileCreation, res: true },
+ { loc: AppPath.WorkflowShowPage, isLogged: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.SyncEmail, res: true },
+ { loc: AppPath.WorkflowShowPage, isLogged: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.InviteTeam, res: true },
+ { loc: AppPath.WorkflowShowPage, isLogged: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.Completed, res: false },
+
{ loc: AppPath.Authorize, isLogged: true, subscriptionStatus: undefined, onboardingStatus: OnboardingStatus.PlanRequired, res: true },
{ loc: AppPath.Authorize, isLogged: true, subscriptionStatus: SubscriptionStatus.Canceled, onboardingStatus: OnboardingStatus.Completed, res: false },
{ loc: AppPath.Authorize, isLogged: true, subscriptionStatus: SubscriptionStatus.Unpaid, onboardingStatus: OnboardingStatus.Completed, res: false },
diff --git a/packages/twenty-front/src/modules/ui/layout/right-drawer/components/RightDrawerRouter.tsx b/packages/twenty-front/src/modules/ui/layout/right-drawer/components/RightDrawerRouter.tsx
index bf5ab29beb..972b2a75e3 100644
--- a/packages/twenty-front/src/modules/ui/layout/right-drawer/components/RightDrawerRouter.tsx
+++ b/packages/twenty-front/src/modules/ui/layout/right-drawer/components/RightDrawerRouter.tsx
@@ -9,10 +9,11 @@ import { isRightDrawerMinimizedState } from '@/ui/layout/right-drawer/states/isR
import { RightDrawerTopBar } from '@/ui/layout/right-drawer/components/RightDrawerTopBar';
import { ComponentByRightDrawerPage } from '@/ui/layout/right-drawer/types/ComponentByRightDrawerPage';
+import { RightDrawerWorkflowEditStep } from '@/workflow/components/RightDrawerWorkflowEditStep';
+import { RightDrawerWorkflowSelectAction } from '@/workflow/components/RightDrawerWorkflowSelectAction';
import { isDefined } from 'twenty-ui';
import { rightDrawerPageState } from '../states/rightDrawerPageState';
import { RightDrawerPages } from '../types/RightDrawerPages';
-import { RightDrawerWorkflow } from '@/workflow/components/RightDrawerWorkflow';
const StyledRightDrawerPage = styled.div`
display: flex;
@@ -36,7 +37,10 @@ const RIGHT_DRAWER_PAGES_CONFIG: ComponentByRightDrawerPage = {
[RightDrawerPages.ViewCalendarEvent]: ,
[RightDrawerPages.ViewRecord]: ,
[RightDrawerPages.Copilot]: ,
- [RightDrawerPages.Workflow]: ,
+ [RightDrawerPages.WorkflowStepSelectAction]: (
+
+ ),
+ [RightDrawerPages.WorkflowStepEdit]: ,
};
export const RightDrawerRouter = () => {
diff --git a/packages/twenty-front/src/modules/ui/layout/right-drawer/constants/RightDrawerPageIcons.ts b/packages/twenty-front/src/modules/ui/layout/right-drawer/constants/RightDrawerPageIcons.ts
index aed0357838..7fc5d9849d 100644
--- a/packages/twenty-front/src/modules/ui/layout/right-drawer/constants/RightDrawerPageIcons.ts
+++ b/packages/twenty-front/src/modules/ui/layout/right-drawer/constants/RightDrawerPageIcons.ts
@@ -5,5 +5,6 @@ export const RIGHT_DRAWER_PAGE_ICONS = {
[RightDrawerPages.ViewCalendarEvent]: 'IconCalendarEvent',
[RightDrawerPages.ViewRecord]: 'Icon123',
[RightDrawerPages.Copilot]: 'IconSparkles',
- [RightDrawerPages.Workflow]: 'IconSparkles',
+ [RightDrawerPages.WorkflowStepEdit]: 'IconSparkles',
+ [RightDrawerPages.WorkflowStepSelectAction]: 'IconSparkles',
};
diff --git a/packages/twenty-front/src/modules/ui/layout/right-drawer/constants/RightDrawerPageTitles.ts b/packages/twenty-front/src/modules/ui/layout/right-drawer/constants/RightDrawerPageTitles.ts
index bb74c9da81..749fb10384 100644
--- a/packages/twenty-front/src/modules/ui/layout/right-drawer/constants/RightDrawerPageTitles.ts
+++ b/packages/twenty-front/src/modules/ui/layout/right-drawer/constants/RightDrawerPageTitles.ts
@@ -5,5 +5,6 @@ export const RIGHT_DRAWER_PAGE_TITLES = {
[RightDrawerPages.ViewCalendarEvent]: 'Calendar Event',
[RightDrawerPages.ViewRecord]: 'Record Editor',
[RightDrawerPages.Copilot]: 'Copilot',
- [RightDrawerPages.Workflow]: 'Workflow',
+ [RightDrawerPages.WorkflowStepEdit]: 'Workflow',
+ [RightDrawerPages.WorkflowStepSelectAction]: 'Workflow',
};
diff --git a/packages/twenty-front/src/modules/ui/layout/right-drawer/types/RightDrawerPages.ts b/packages/twenty-front/src/modules/ui/layout/right-drawer/types/RightDrawerPages.ts
index 2217d437f4..f016669b48 100644
--- a/packages/twenty-front/src/modules/ui/layout/right-drawer/types/RightDrawerPages.ts
+++ b/packages/twenty-front/src/modules/ui/layout/right-drawer/types/RightDrawerPages.ts
@@ -3,5 +3,6 @@ export enum RightDrawerPages {
ViewCalendarEvent = 'view-calendar-event',
ViewRecord = 'view-record',
Copilot = 'copilot',
- Workflow = 'workflow',
+ WorkflowStepSelectAction = 'workflow-step-select-action',
+ WorkflowStepEdit = 'workflow-step-edit',
}
diff --git a/packages/twenty-front/src/modules/workflow/components/RightDrawerWorkflow.tsx b/packages/twenty-front/src/modules/workflow/components/RightDrawerWorkflow.tsx
deleted file mode 100644
index b12e749a89..0000000000
--- a/packages/twenty-front/src/modules/workflow/components/RightDrawerWorkflow.tsx
+++ /dev/null
@@ -1,40 +0,0 @@
-import styled from '@emotion/styled';
-
-const StyledContainer = styled.div`
- box-sizing: border-box;
- display: flex;
- flex-direction: column;
- height: 100%;
- justify-content: flex-start;
- overflow-y: auto;
- position: relative;
-`;
-
-const StyledChatArea = styled.div`
- flex: 1;
- display: flex;
- flex-direction: column;
- overflow-y: scroll;
- padding: ${({ theme }) => theme.spacing(6)};
- padding-bottom: 0px;
-`;
-
-const StyledNewMessageArea = styled.div`
- display: flex;
- flex-direction: column;
- padding: ${({ theme }) => theme.spacing(6)};
- padding-top: 0px;
-`;
-
-export const RightDrawerWorkflow = () => {
- const handleCreateCodeBlock = () => {};
-
- return (
-
- {/* TODO */}
-
-
-
-
- );
-};
diff --git a/packages/twenty-front/src/modules/workflow/components/RightDrawerWorkflowEditStep.tsx b/packages/twenty-front/src/modules/workflow/components/RightDrawerWorkflowEditStep.tsx
new file mode 100644
index 0000000000..96b82e2f00
--- /dev/null
+++ b/packages/twenty-front/src/modules/workflow/components/RightDrawerWorkflowEditStep.tsx
@@ -0,0 +1,10 @@
+import { showPageWorkflowSelectedNodeState } from '@/workflow/states/showPageWorkflowSelectedNodeState';
+import { useRecoilValue } from 'recoil';
+
+export const RightDrawerWorkflowEditStep = () => {
+ const showPageWorkflowSelectedNode = useRecoilValue(
+ showPageWorkflowSelectedNodeState,
+ );
+
+ return
{showPageWorkflowSelectedNode}
;
+};
diff --git a/packages/twenty-front/src/modules/workflow/components/RightDrawerWorkflowSelectAction.tsx b/packages/twenty-front/src/modules/workflow/components/RightDrawerWorkflowSelectAction.tsx
new file mode 100644
index 0000000000..d99189f66f
--- /dev/null
+++ b/packages/twenty-front/src/modules/workflow/components/RightDrawerWorkflowSelectAction.tsx
@@ -0,0 +1,28 @@
+import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
+import { useFindOneRecord } from '@/object-record/hooks/useFindOneRecord';
+import { RightDrawerWorkflowSelectActionContent } from '@/workflow/components/RightDrawerWorkflowSelectActionContent';
+import { showPageWorkflowIdState } from '@/workflow/states/showPageWorkflowIdState';
+import { Workflow } from '@/workflow/types/Workflow';
+import { useRecoilValue } from 'recoil';
+import { isDefined } from 'twenty-ui';
+
+export const RightDrawerWorkflowSelectAction = () => {
+ const showPageWorkflowId = useRecoilValue(showPageWorkflowIdState);
+
+ const { record: workflow } = useFindOneRecord({
+ objectNameSingular: CoreObjectNameSingular.Workflow,
+ objectRecordId: showPageWorkflowId,
+ recordGqlFields: {
+ id: true,
+ name: true,
+ versions: true,
+ publishedVersionId: true,
+ },
+ });
+
+ if (!isDefined(workflow)) {
+ return null;
+ }
+
+ return ;
+};
diff --git a/packages/twenty-front/src/modules/workflow/components/RightDrawerWorkflowSelectActionContent.tsx b/packages/twenty-front/src/modules/workflow/components/RightDrawerWorkflowSelectActionContent.tsx
new file mode 100644
index 0000000000..bc888238b2
--- /dev/null
+++ b/packages/twenty-front/src/modules/workflow/components/RightDrawerWorkflowSelectActionContent.tsx
@@ -0,0 +1,60 @@
+import { TabList } from '@/ui/layout/tab/components/TabList';
+import { MenuItem } from '@/ui/navigation/menu-item/components/MenuItem';
+import { useRightDrawerWorkflowSelectAction } from '@/workflow/hooks/useRightDrawerWorkflowSelectAction';
+import { Workflow } from '@/workflow/types/Workflow';
+import styled from '@emotion/styled';
+
+// FIXME: copy-pasted
+const StyledTabListContainer = styled.div`
+ align-items: center;
+ border-bottom: ${({ theme }) => `1px solid ${theme.border.color.light}`};
+ box-sizing: border-box;
+ display: flex;
+ gap: ${({ theme }) => theme.spacing(2)};
+ height: 40px;
+`;
+
+const StyledActionListContainer = styled.div`
+ display: flex;
+ flex-direction: column;
+ height: 100%;
+ overflow-y: auto;
+
+ padding-block: ${({ theme }) => theme.spacing(1)};
+ padding-inline: ${({ theme }) => theme.spacing(2)};
+`;
+
+export const TAB_LIST_COMPONENT_ID =
+ 'workflow-select-action-page-right-tab-list';
+
+export const RightDrawerWorkflowSelectActionContent = ({
+ workflow,
+}: {
+ workflow: Workflow;
+}) => {
+ const tabListId = `${TAB_LIST_COMPONENT_ID}`;
+
+ const { tabs, options, handleActionClick } =
+ useRightDrawerWorkflowSelectAction({ tabListId, workflow });
+
+ return (
+ <>
+
+
+
+
+
+ {options.map((option) => (
+
+ >
+ );
+};
diff --git a/packages/twenty-front/src/modules/workflow/components/WorkflowShowPageDiagram.tsx b/packages/twenty-front/src/modules/workflow/components/WorkflowShowPageDiagram.tsx
index 090d074ad1..9643aaa7a0 100644
--- a/packages/twenty-front/src/modules/workflow/components/WorkflowShowPageDiagram.tsx
+++ b/packages/twenty-front/src/modules/workflow/components/WorkflowShowPageDiagram.tsx
@@ -1,4 +1,5 @@
-import { WorkflowShowPageDiagramCreateStepNode } from '@/workflow/components/WorkflowShowPageDiagramCreateStepNode.tsx';
+import { WorkflowShowPageDiagramCreateStepNode } from '@/workflow/components/WorkflowShowPageDiagramCreateStepNode';
+import { WorkflowShowPageDiagramEffect } from '@/workflow/components/WorkflowShowPageDiagramEffect';
import { WorkflowShowPageDiagramStepNode } from '@/workflow/components/WorkflowShowPageDiagramStepNode';
import { showPageWorkflowDiagramState } from '@/workflow/states/showPageWorkflowDiagramState';
import {
@@ -80,6 +81,8 @@ export const WorkflowShowPageDiagram = ({
onNodesChange={handleNodesChange}
onEdgesChange={handleEdgesChange}
>
+
+
);
diff --git a/packages/twenty-front/src/modules/workflow/components/WorkflowShowPageDiagramCreateStepNode.tsx.tsx b/packages/twenty-front/src/modules/workflow/components/WorkflowShowPageDiagramCreateStepNode.tsx
similarity index 50%
rename from packages/twenty-front/src/modules/workflow/components/WorkflowShowPageDiagramCreateStepNode.tsx.tsx
rename to packages/twenty-front/src/modules/workflow/components/WorkflowShowPageDiagramCreateStepNode.tsx
index a799480e81..95fb6e8366 100644
--- a/packages/twenty-front/src/modules/workflow/components/WorkflowShowPageDiagramCreateStepNode.tsx.tsx
+++ b/packages/twenty-front/src/modules/workflow/components/WorkflowShowPageDiagramCreateStepNode.tsx
@@ -1,6 +1,4 @@
import { IconButton } from '@/ui/input/button/components/IconButton';
-import { useRightDrawer } from '@/ui/layout/right-drawer/hooks/useRightDrawer';
-import { RightDrawerPages } from '@/ui/layout/right-drawer/types/RightDrawerPages';
import styled from '@emotion/styled';
import { Handle, Position } from '@xyflow/react';
import { IconPlus } from 'twenty-ui';
@@ -10,17 +8,11 @@ export const StyledTargetHandle = styled(Handle)`
`;
export const WorkflowShowPageDiagramCreateStepNode = () => {
- const { openRightDrawer } = useRightDrawer();
-
- const handleCreateStepNodeButtonClick = () => {
- openRightDrawer(RightDrawerPages.Workflow);
- };
-
return (
-
+ <>
-
-
+
+ >
);
};
diff --git a/packages/twenty-front/src/modules/workflow/components/WorkflowShowPageDiagramEffect.tsx b/packages/twenty-front/src/modules/workflow/components/WorkflowShowPageDiagramEffect.tsx
new file mode 100644
index 0000000000..227d66a8d5
--- /dev/null
+++ b/packages/twenty-front/src/modules/workflow/components/WorkflowShowPageDiagramEffect.tsx
@@ -0,0 +1,81 @@
+import { useRightDrawer } from '@/ui/layout/right-drawer/hooks/useRightDrawer';
+import { RightDrawerPages } from '@/ui/layout/right-drawer/types/RightDrawerPages';
+import { useStartNodeCreation } from '@/workflow/hooks/useStartNodeCreation';
+import { showPageWorkflowDiagramTriggerNodeSelectionState } from '@/workflow/states/showPageWorkflowDiagramTriggerNodeSelectionState';
+import { showPageWorkflowSelectedNodeState } from '@/workflow/states/showPageWorkflowSelectedNodeState';
+import {
+ WorkflowDiagramEdge,
+ WorkflowDiagramNode,
+} from '@/workflow/types/WorkflowDiagram';
+import {
+ OnSelectionChangeParams,
+ useOnSelectionChange,
+ useReactFlow,
+} from '@xyflow/react';
+import { useCallback, useEffect } from 'react';
+import { useRecoilValue, useSetRecoilState } from 'recoil';
+import { isDefined } from 'twenty-ui';
+
+export const WorkflowShowPageDiagramEffect = () => {
+ const reactflow = useReactFlow();
+
+ const { startNodeCreation } = useStartNodeCreation();
+
+ const { openRightDrawer, closeRightDrawer } = useRightDrawer();
+ const setShowPageWorkflowSelectedNode = useSetRecoilState(
+ showPageWorkflowSelectedNodeState,
+ );
+
+ const showPageWorkflowDiagramTriggerNodeSelection = useRecoilValue(
+ showPageWorkflowDiagramTriggerNodeSelectionState,
+ );
+
+ const handleSelectionChange = useCallback(
+ ({ nodes }: OnSelectionChangeParams) => {
+ const selectedNode = nodes[0] as WorkflowDiagramNode;
+ const isClosingStep = isDefined(selectedNode) === false;
+
+ if (isClosingStep) {
+ closeRightDrawer();
+
+ return;
+ }
+
+ const isCreateStepNode = selectedNode.type === 'create-step';
+ if (isCreateStepNode) {
+ if (selectedNode.data.nodeType !== 'create-step') {
+ throw new Error('Expected selected node to be a create step node.');
+ }
+
+ startNodeCreation(selectedNode.data.parentNodeId);
+
+ return;
+ }
+
+ setShowPageWorkflowSelectedNode(selectedNode.id);
+ openRightDrawer(RightDrawerPages.WorkflowStepEdit);
+ },
+ [
+ closeRightDrawer,
+ openRightDrawer,
+ setShowPageWorkflowSelectedNode,
+ startNodeCreation,
+ ],
+ );
+
+ useOnSelectionChange({
+ onChange: handleSelectionChange,
+ });
+
+ useEffect(() => {
+ if (!isDefined(showPageWorkflowDiagramTriggerNodeSelection)) {
+ return;
+ }
+
+ reactflow.updateNode(showPageWorkflowDiagramTriggerNodeSelection, {
+ selected: true,
+ });
+ }, [reactflow, showPageWorkflowDiagramTriggerNodeSelection]);
+
+ return null;
+};
diff --git a/packages/twenty-front/src/modules/workflow/components/WorkflowShowPageEffect.tsx b/packages/twenty-front/src/modules/workflow/components/WorkflowShowPageEffect.tsx
index 6d41e5d7fd..56d27ffb70 100644
--- a/packages/twenty-front/src/modules/workflow/components/WorkflowShowPageEffect.tsx
+++ b/packages/twenty-front/src/modules/workflow/components/WorkflowShowPageEffect.tsx
@@ -2,6 +2,7 @@ import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSi
import { useFindOneRecord } from '@/object-record/hooks/useFindOneRecord';
import { showPageWorkflowDiagramState } from '@/workflow/states/showPageWorkflowDiagramState';
import { showPageWorkflowErrorState } from '@/workflow/states/showPageWorkflowErrorState';
+import { showPageWorkflowIdState } from '@/workflow/states/showPageWorkflowIdState';
import { showPageWorkflowLoadingState } from '@/workflow/states/showPageWorkflowLoadingState';
import { Workflow } from '@/workflow/types/Workflow';
import { addCreateStepNodes } from '@/workflow/utils/addCreateStepNodes';
@@ -32,6 +33,7 @@ export const WorkflowShowPageEffect = ({
},
});
+ const setShowPageWorkflowId = useSetRecoilState(showPageWorkflowIdState);
const setCurrentWorkflowData = useSetRecoilState(
showPageWorkflowDiagramState,
);
@@ -40,6 +42,10 @@ export const WorkflowShowPageEffect = ({
);
const setCurrentWorkflowError = useSetRecoilState(showPageWorkflowErrorState);
+ useEffect(() => {
+ setShowPageWorkflowId(workflowId);
+ }, [setShowPageWorkflowId, workflowId]);
+
useEffect(() => {
const flowLastVersion = getWorkflowLastDiagramVersion(workflow);
const flowWithCreateStepNodes = addCreateStepNodes(flowLastVersion);
diff --git a/packages/twenty-front/src/modules/workflow/hooks/useCreateNode.tsx b/packages/twenty-front/src/modules/workflow/hooks/useCreateNode.tsx
new file mode 100644
index 0000000000..c4873c9a33
--- /dev/null
+++ b/packages/twenty-front/src/modules/workflow/hooks/useCreateNode.tsx
@@ -0,0 +1,47 @@
+import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
+import { useUpdateOneRecord } from '@/object-record/hooks/useUpdateOneRecord';
+import {
+ Workflow,
+ WorkflowStep,
+ WorkflowVersion,
+} from '@/workflow/types/Workflow';
+import { getWorkflowLastVersion } from '@/workflow/utils/getWorkflowLastVersion';
+import { insertStep } from '@/workflow/utils/insertStep';
+import { isDefined } from 'twenty-ui';
+
+export const useCreateNode = ({ workflow }: { workflow: Workflow }) => {
+ const { updateOneRecord: updateOneWorkflowVersion } =
+ useUpdateOneRecord({
+ objectNameSingular: CoreObjectNameSingular.WorkflowVersion,
+ });
+
+ const createNode = ({
+ parentNodeId,
+ nodeToAdd,
+ }: {
+ parentNodeId: string;
+ nodeToAdd: WorkflowStep;
+ }) => {
+ const lastVersion = getWorkflowLastVersion(workflow);
+ if (!isDefined(lastVersion)) {
+ throw new Error(
+ "Can't add a node when no version exists yet. Create a first workflow version before trying to add a node.",
+ );
+ }
+
+ return updateOneWorkflowVersion({
+ idToUpdate: lastVersion.id,
+ updateOneRecordInput: {
+ steps: insertStep({
+ steps: lastVersion.steps,
+ parentStepId: parentNodeId,
+ stepToAdd: nodeToAdd,
+ }),
+ },
+ });
+ };
+
+ return {
+ createNode,
+ };
+};
diff --git a/packages/twenty-front/src/modules/workflow/hooks/useRightDrawerWorkflowSelectAction.tsx b/packages/twenty-front/src/modules/workflow/hooks/useRightDrawerWorkflowSelectAction.tsx
new file mode 100644
index 0000000000..5284de94dd
--- /dev/null
+++ b/packages/twenty-front/src/modules/workflow/hooks/useRightDrawerWorkflowSelectAction.tsx
@@ -0,0 +1,117 @@
+import { useTabList } from '@/ui/layout/tab/hooks/useTabList';
+import { useCreateNode } from '@/workflow/hooks/useCreateNode';
+import { showPageWorkflowDiagramTriggerNodeSelectionState } from '@/workflow/states/showPageWorkflowDiagramTriggerNodeSelectionState';
+import { workflowCreateStepFromParentStepIdState } from '@/workflow/states/workflowCreateStepFromParentStepIdState';
+import { Workflow } from '@/workflow/types/Workflow';
+import { useRecoilValue, useSetRecoilState } from 'recoil';
+import {
+ IconPlaystationSquare,
+ IconPlug,
+ IconPlus,
+ IconSearch,
+ IconSettingsAutomation,
+} from 'twenty-ui';
+import { v4 } from 'uuid';
+
+export const useRightDrawerWorkflowSelectAction = ({
+ tabListId,
+ workflow,
+}: {
+ tabListId: string;
+ workflow: Workflow;
+}) => {
+ const workflowCreateStepFromParentStepId = useRecoilValue(
+ workflowCreateStepFromParentStepIdState,
+ );
+
+ const setShowPageWorkflowDiagramTriggerNodeSelection = useSetRecoilState(
+ showPageWorkflowDiagramTriggerNodeSelectionState,
+ );
+
+ const { createNode } = useCreateNode({ workflow });
+
+ const allOptions: Array<{
+ id: string;
+ name: string;
+ type: 'standard' | 'custom';
+ icon: any;
+ }> = [
+ {
+ id: 'create-record',
+ name: 'Create Record',
+ type: 'standard',
+ icon: IconPlus,
+ },
+ {
+ id: 'find-records',
+ name: 'Find Records',
+ type: 'standard',
+ icon: IconSearch,
+ },
+ ];
+
+ const tabs = [
+ {
+ id: 'all',
+ title: 'All',
+ Icon: IconSettingsAutomation,
+ },
+ {
+ id: 'standard',
+ title: 'Standard',
+ Icon: IconPlaystationSquare,
+ },
+ {
+ id: 'custom',
+ title: 'Custom',
+ Icon: IconPlug,
+ },
+ ];
+
+ const { activeTabIdState } = useTabList(tabListId);
+ const activeTabId = useRecoilValue(activeTabIdState);
+
+ const options = allOptions.filter(
+ (option) => activeTabId === 'all' || option.type === activeTabId,
+ );
+
+ const handleActionClick = async (actionId: string) => {
+ if (workflowCreateStepFromParentStepId === undefined) {
+ throw new Error('Select a step to create a new step from first.');
+ }
+
+ const newNodeId = v4();
+
+ /**
+ * FIXME: For now, the data of the node to create are mostly static.
+ */
+ await createNode({
+ parentNodeId: workflowCreateStepFromParentStepId,
+ nodeToAdd: {
+ id: newNodeId,
+ name: actionId,
+ type: 'CODE_ACTION',
+ valid: true,
+ settings: {
+ serverlessFunctionId: '111',
+ errorHandlingOptions: {
+ continueOnFailure: {
+ value: true,
+ },
+ retryOnFailure: {
+ value: true,
+ },
+ },
+ },
+ },
+ });
+
+ setShowPageWorkflowDiagramTriggerNodeSelection(newNodeId);
+ };
+
+ return {
+ tabs,
+ options,
+ handleActionClick,
+ };
+};
diff --git a/packages/twenty-front/src/modules/workflow/hooks/useStartNodeCreation.tsx b/packages/twenty-front/src/modules/workflow/hooks/useStartNodeCreation.tsx
new file mode 100644
index 0000000000..54e479cfb6
--- /dev/null
+++ b/packages/twenty-front/src/modules/workflow/hooks/useStartNodeCreation.tsx
@@ -0,0 +1,29 @@
+import { useRightDrawer } from '@/ui/layout/right-drawer/hooks/useRightDrawer';
+import { RightDrawerPages } from '@/ui/layout/right-drawer/types/RightDrawerPages';
+import { workflowCreateStepFromParentStepIdState } from '@/workflow/states/workflowCreateStepFromParentStepIdState';
+import { useCallback } from 'react';
+import { useSetRecoilState } from 'recoil';
+
+export const useStartNodeCreation = () => {
+ const { openRightDrawer } = useRightDrawer();
+ const setWorkflowCreateStepFromParentStepId = useSetRecoilState(
+ workflowCreateStepFromParentStepIdState,
+ );
+
+ /**
+ * This function is used in a context where dependencies shouldn't change much.
+ * That's why its wrapped in a `useCallback` hook. Removing memoization might break the app unexpectedly.
+ */
+ const startNodeCreation = useCallback(
+ (parentNodeId: string) => {
+ setWorkflowCreateStepFromParentStepId(parentNodeId);
+
+ openRightDrawer(RightDrawerPages.WorkflowStepSelectAction);
+ },
+ [openRightDrawer, setWorkflowCreateStepFromParentStepId],
+ );
+
+ return {
+ startNodeCreation,
+ };
+};
diff --git a/packages/twenty-front/src/modules/workflow/states/showPageWorkflowDiagramTriggerNodeSelectionState.ts b/packages/twenty-front/src/modules/workflow/states/showPageWorkflowDiagramTriggerNodeSelectionState.ts
new file mode 100644
index 0000000000..780b57f3cb
--- /dev/null
+++ b/packages/twenty-front/src/modules/workflow/states/showPageWorkflowDiagramTriggerNodeSelectionState.ts
@@ -0,0 +1,8 @@
+import { createState } from 'twenty-ui';
+
+export const showPageWorkflowDiagramTriggerNodeSelectionState = createState<
+ string | undefined
+>({
+ key: 'showPageWorkflowDiagramTriggerNodeSelectionState',
+ defaultValue: undefined,
+});
diff --git a/packages/twenty-front/src/modules/workflow/states/showPageWorkflowIdState.ts b/packages/twenty-front/src/modules/workflow/states/showPageWorkflowIdState.ts
new file mode 100644
index 0000000000..a481713abd
--- /dev/null
+++ b/packages/twenty-front/src/modules/workflow/states/showPageWorkflowIdState.ts
@@ -0,0 +1,6 @@
+import { createState } from 'twenty-ui';
+
+export const showPageWorkflowIdState = createState({
+ key: 'showPageWorkflowIdState',
+ defaultValue: undefined,
+});
diff --git a/packages/twenty-front/src/modules/workflow/states/showPageWorkflowSelectedNodeState.ts b/packages/twenty-front/src/modules/workflow/states/showPageWorkflowSelectedNodeState.ts
new file mode 100644
index 0000000000..616c61c6e8
--- /dev/null
+++ b/packages/twenty-front/src/modules/workflow/states/showPageWorkflowSelectedNodeState.ts
@@ -0,0 +1,8 @@
+import { createState } from 'twenty-ui';
+
+export const showPageWorkflowSelectedNodeState = createState<
+ string | undefined
+>({
+ key: 'showPageWorkflowSelectedNodeState',
+ defaultValue: undefined,
+});
diff --git a/packages/twenty-front/src/modules/workflow/states/workflowCreateStepFromParentStepIdState.ts b/packages/twenty-front/src/modules/workflow/states/workflowCreateStepFromParentStepIdState.ts
new file mode 100644
index 0000000000..4e64010b96
--- /dev/null
+++ b/packages/twenty-front/src/modules/workflow/states/workflowCreateStepFromParentStepIdState.ts
@@ -0,0 +1,8 @@
+import { createState } from 'twenty-ui';
+
+export const workflowCreateStepFromParentStepIdState = createState<
+ string | undefined
+>({
+ key: 'workflowCreateStepFromParentStepId',
+ defaultValue: undefined,
+});
diff --git a/packages/twenty-front/src/modules/workflow/types/WorkflowDiagram.ts b/packages/twenty-front/src/modules/workflow/types/WorkflowDiagram.ts
index f97d5027ed..237daab24b 100644
--- a/packages/twenty-front/src/modules/workflow/types/WorkflowDiagram.ts
+++ b/packages/twenty-front/src/modules/workflow/types/WorkflowDiagram.ts
@@ -13,7 +13,10 @@ export type WorkflowDiagramStepNodeData = {
label: string;
};
-export type WorkflowDiagramCreateStepNodeData = Record;
+export type WorkflowDiagramCreateStepNodeData = {
+ nodeType: 'create-step';
+ parentNodeId: string;
+};
export type WorkflowDiagramNodeData =
| WorkflowDiagramStepNodeData
diff --git a/packages/twenty-front/src/modules/workflow/utils/__tests__/generateWorkflowDiagram.test.ts b/packages/twenty-front/src/modules/workflow/utils/__tests__/generateWorkflowDiagram.test.ts
index 0716aa29bb..8853935765 100644
--- a/packages/twenty-front/src/modules/workflow/utils/__tests__/generateWorkflowDiagram.test.ts
+++ b/packages/twenty-front/src/modules/workflow/utils/__tests__/generateWorkflowDiagram.test.ts
@@ -70,8 +70,10 @@ describe('generateWorkflowDiagram', () => {
const stepNodes = result.nodes.slice(1);
for (const [index, step] of steps.entries()) {
- expect(stepNodes[index].data.nodeType).toBe('action');
- expect(stepNodes[index].data.label).toBe(step.name);
+ expect(stepNodes[index].data).toEqual({
+ nodeType: 'action',
+ label: step.name,
+ });
}
});
diff --git a/packages/twenty-front/src/modules/workflow/utils/__tests__/insertStep.test.ts b/packages/twenty-front/src/modules/workflow/utils/__tests__/insertStep.test.ts
new file mode 100644
index 0000000000..4479abe6f9
--- /dev/null
+++ b/packages/twenty-front/src/modules/workflow/utils/__tests__/insertStep.test.ts
@@ -0,0 +1,217 @@
+import { WorkflowStep, WorkflowVersion } from '@/workflow/types/Workflow';
+import { insertStep } from '../insertStep';
+
+describe('insertStep', () => {
+ it('returns a deep copy of the provided steps array instead of mutating it', () => {
+ const workflowVersionInitial: WorkflowVersion = {
+ __typename: 'WorkflowVersion',
+ createdAt: '',
+ id: '1',
+ name: '',
+ steps: [],
+ trigger: {
+ settings: { eventName: 'company.created' },
+ type: 'DATABASE_EVENT',
+ },
+ updatedAt: '',
+ workflowId: '',
+ };
+ const stepToAdd: WorkflowStep = {
+ id: 'step-1',
+ name: '',
+ settings: {
+ errorHandlingOptions: {
+ retryOnFailure: { value: true },
+ continueOnFailure: { value: false },
+ },
+ serverlessFunctionId: 'a5434be2-c10b-465c-acec-46492782a997',
+ },
+ type: 'CODE_ACTION',
+ valid: true,
+ };
+
+ const stepsUpdated = insertStep({
+ steps: workflowVersionInitial.steps,
+ stepToAdd,
+ parentStepId: undefined,
+ });
+
+ expect(workflowVersionInitial.steps).not.toBe(stepsUpdated);
+ });
+
+ it('adds the step when the steps array is empty', () => {
+ const workflowVersionInitial: WorkflowVersion = {
+ __typename: 'WorkflowVersion',
+ createdAt: '',
+ id: '1',
+ name: '',
+ steps: [],
+ trigger: {
+ settings: { eventName: 'company.created' },
+ type: 'DATABASE_EVENT',
+ },
+ updatedAt: '',
+ workflowId: '',
+ };
+ const stepToAdd: WorkflowStep = {
+ id: 'step-1',
+ name: '',
+ settings: {
+ errorHandlingOptions: {
+ retryOnFailure: { value: true },
+ continueOnFailure: { value: false },
+ },
+ serverlessFunctionId: 'a5434be2-c10b-465c-acec-46492782a997',
+ },
+ type: 'CODE_ACTION',
+ valid: true,
+ };
+
+ const stepsUpdated = insertStep({
+ steps: workflowVersionInitial.steps,
+ stepToAdd,
+ parentStepId: undefined,
+ });
+
+ const expectedUpdatedSteps: Array = [stepToAdd];
+ expect(stepsUpdated).toEqual(expectedUpdatedSteps);
+ });
+
+ it('adds the step at the end of a non-empty steps array', () => {
+ const workflowVersionInitial: WorkflowVersion = {
+ __typename: 'WorkflowVersion',
+ createdAt: '',
+ id: '1',
+ name: '',
+ steps: [
+ {
+ id: 'step-1',
+ name: '',
+ settings: {
+ errorHandlingOptions: {
+ retryOnFailure: { value: true },
+ continueOnFailure: { value: false },
+ },
+ serverlessFunctionId: 'a5434be2-c10b-465c-acec-46492782a997',
+ },
+ type: 'CODE_ACTION',
+ valid: true,
+ },
+ {
+ id: 'step-2',
+ name: '',
+ settings: {
+ errorHandlingOptions: {
+ retryOnFailure: { value: true },
+ continueOnFailure: { value: false },
+ },
+ serverlessFunctionId: 'a5434be2-c10b-465c-acec-46492782a997',
+ },
+ type: 'CODE_ACTION',
+ valid: true,
+ },
+ ],
+ trigger: {
+ settings: { eventName: 'company.created' },
+ type: 'DATABASE_EVENT',
+ },
+ updatedAt: '',
+ workflowId: '',
+ };
+ const stepToAdd: WorkflowStep = {
+ id: 'step-3',
+ name: '',
+ settings: {
+ errorHandlingOptions: {
+ retryOnFailure: { value: true },
+ continueOnFailure: { value: false },
+ },
+ serverlessFunctionId: 'a5434be2-c10b-465c-acec-46492782a997',
+ },
+ type: 'CODE_ACTION',
+ valid: true,
+ };
+
+ const stepsUpdated = insertStep({
+ steps: workflowVersionInitial.steps,
+ stepToAdd,
+ parentStepId: workflowVersionInitial.steps[1].id, // Note the selected step.
+ });
+
+ const expectedUpdatedSteps: Array = [
+ workflowVersionInitial.steps[0],
+ workflowVersionInitial.steps[1],
+ stepToAdd,
+ ];
+ expect(stepsUpdated).toEqual(expectedUpdatedSteps);
+ });
+
+ it('adds the step in the middle of a non-empty steps array', () => {
+ const workflowVersionInitial: WorkflowVersion = {
+ __typename: 'WorkflowVersion',
+ createdAt: '',
+ id: '1',
+ name: '',
+ steps: [
+ {
+ id: 'step-1',
+ name: '',
+ settings: {
+ errorHandlingOptions: {
+ retryOnFailure: { value: true },
+ continueOnFailure: { value: false },
+ },
+ serverlessFunctionId: 'a5434be2-c10b-465c-acec-46492782a997',
+ },
+ type: 'CODE_ACTION',
+ valid: true,
+ },
+ {
+ id: 'step-2',
+ name: '',
+ settings: {
+ errorHandlingOptions: {
+ retryOnFailure: { value: true },
+ continueOnFailure: { value: false },
+ },
+ serverlessFunctionId: 'a5434be2-c10b-465c-acec-46492782a997',
+ },
+ type: 'CODE_ACTION',
+ valid: true,
+ },
+ ],
+ trigger: {
+ settings: { eventName: 'company.created' },
+ type: 'DATABASE_EVENT',
+ },
+ updatedAt: '',
+ workflowId: '',
+ };
+ const stepToAdd: WorkflowStep = {
+ id: 'step-3',
+ name: '',
+ settings: {
+ errorHandlingOptions: {
+ retryOnFailure: { value: true },
+ continueOnFailure: { value: false },
+ },
+ serverlessFunctionId: 'a5434be2-c10b-465c-acec-46492782a997',
+ },
+ type: 'CODE_ACTION',
+ valid: true,
+ };
+
+ const stepsUpdated = insertStep({
+ steps: workflowVersionInitial.steps,
+ stepToAdd,
+ parentStepId: workflowVersionInitial.steps[0].id, // Note the selected step.
+ });
+
+ const expectedUpdatedSteps: Array = [
+ workflowVersionInitial.steps[0],
+ stepToAdd,
+ workflowVersionInitial.steps[1],
+ ];
+ expect(stepsUpdated).toEqual(expectedUpdatedSteps);
+ });
+});
diff --git a/packages/twenty-front/src/modules/workflow/utils/addCreateStepNodes.ts b/packages/twenty-front/src/modules/workflow/utils/addCreateStepNodes.ts
index 9d2941adb2..381d058898 100644
--- a/packages/twenty-front/src/modules/workflow/utils/addCreateStepNodes.ts
+++ b/packages/twenty-front/src/modules/workflow/utils/addCreateStepNodes.ts
@@ -18,7 +18,10 @@ export const addCreateStepNodes = ({ nodes, edges }: WorkflowDiagram) => {
const newCreateStepNode: WorkflowDiagramNode = {
id: v4(),
type: 'create-step',
- data: {},
+ data: {
+ nodeType: 'create-step',
+ parentNodeId: node.id,
+ },
position: { x: 0, y: 0 },
};
diff --git a/packages/twenty-front/src/modules/workflow/utils/generateWorkflowDiagram.ts b/packages/twenty-front/src/modules/workflow/utils/generateWorkflowDiagram.ts
index 6bb95f23cb..6399bb995b 100644
--- a/packages/twenty-front/src/modules/workflow/utils/generateWorkflowDiagram.ts
+++ b/packages/twenty-front/src/modules/workflow/utils/generateWorkflowDiagram.ts
@@ -24,7 +24,7 @@ export const generateWorkflowDiagram = ({
xPos: number,
yPos: number,
) => {
- const nodeId = v4();
+ const nodeId = step.id;
nodes.push({
id: nodeId,
data: {
@@ -58,7 +58,7 @@ export const generateWorkflowDiagram = ({
};
// Start with the trigger node
- const triggerNodeId = v4();
+ const triggerNodeId = 'trigger';
nodes.push({
id: triggerNodeId,
data: {
diff --git a/packages/twenty-front/src/modules/workflow/utils/getOrganizedDiagram.ts b/packages/twenty-front/src/modules/workflow/utils/getOrganizedDiagram.ts
index 0aa687d710..a5d549c71a 100644
--- a/packages/twenty-front/src/modules/workflow/utils/getOrganizedDiagram.ts
+++ b/packages/twenty-front/src/modules/workflow/utils/getOrganizedDiagram.ts
@@ -1,9 +1,6 @@
import { WorkflowDiagram } from '@/workflow/types/WorkflowDiagram';
import Dagre from '@dagrejs/dagre';
-/**
- * Set the position of the nodes in the diagram. The positions are computed with a layouting algorithm.
- */
export const getOrganizedDiagram = (
diagram: WorkflowDiagram,
): WorkflowDiagram => {
diff --git a/packages/twenty-front/src/modules/workflow/utils/getWorkflowLastDiagramVersion.ts b/packages/twenty-front/src/modules/workflow/utils/getWorkflowLastDiagramVersion.ts
index 448ed7fec4..7e4d13a693 100644
--- a/packages/twenty-front/src/modules/workflow/utils/getWorkflowLastDiagramVersion.ts
+++ b/packages/twenty-front/src/modules/workflow/utils/getWorkflowLastDiagramVersion.ts
@@ -1,6 +1,7 @@
import { Workflow } from '@/workflow/types/Workflow';
import { WorkflowDiagram } from '@/workflow/types/WorkflowDiagram';
import { generateWorkflowDiagram } from '@/workflow/utils/generateWorkflowDiagram';
+import { getWorkflowLastVersion } from '@/workflow/utils/getWorkflowLastVersion';
import { isDefined } from 'twenty-ui';
const EMPTY_DIAGRAM: WorkflowDiagram = {
@@ -15,7 +16,7 @@ export const getWorkflowLastDiagramVersion = (
return EMPTY_DIAGRAM;
}
- const lastVersion = workflow.versions.at(-1);
+ const lastVersion = getWorkflowLastVersion(workflow);
if (!isDefined(lastVersion) || !isDefined(lastVersion.trigger)) {
return EMPTY_DIAGRAM;
}
diff --git a/packages/twenty-front/src/modules/workflow/utils/getWorkflowLastVersion.ts b/packages/twenty-front/src/modules/workflow/utils/getWorkflowLastVersion.ts
new file mode 100644
index 0000000000..b69790e9c4
--- /dev/null
+++ b/packages/twenty-front/src/modules/workflow/utils/getWorkflowLastVersion.ts
@@ -0,0 +1,10 @@
+import { Workflow, WorkflowVersion } from '@/workflow/types/Workflow';
+
+export const getWorkflowLastVersion = (
+ workflow: Workflow,
+): WorkflowVersion | undefined => {
+ return workflow.versions
+ .slice()
+ .sort((a, b) => (a.createdAt < b.createdAt ? -1 : 1))
+ .at(-1);
+};
diff --git a/packages/twenty-front/src/modules/workflow/utils/insertStep.ts b/packages/twenty-front/src/modules/workflow/utils/insertStep.ts
new file mode 100644
index 0000000000..038402e789
--- /dev/null
+++ b/packages/twenty-front/src/modules/workflow/utils/insertStep.ts
@@ -0,0 +1,61 @@
+import { WorkflowStep } from '@/workflow/types/Workflow';
+
+const findStepPositionOrThrow = ({
+ steps,
+ stepId,
+}: {
+ steps: Array;
+ stepId: string | undefined;
+}): { steps: Array; index: number } => {
+ if (stepId === undefined) {
+ return {
+ steps,
+ index: 0,
+ };
+ }
+
+ for (const [index, step] of steps.entries()) {
+ if (step.id === stepId) {
+ return {
+ steps,
+ index,
+ };
+ }
+
+ // TODO: When condition will have been implemented, put recursivity here.
+ // if (step.type === "CONDITION") {
+ // return findNodePosition({
+ // workflowSteps: step.conditions,
+ // stepId,
+ // })
+ // }
+ }
+
+ throw new Error(`Couldn't locate the step. Unreachable step id: ${stepId}.`);
+};
+
+export const insertStep = ({
+ steps: stepsInitial,
+ stepToAdd,
+ parentStepId,
+}: {
+ steps: Array;
+ parentStepId: string | undefined;
+ stepToAdd: WorkflowStep;
+}): Array => {
+ // Make a deep copy of the nested object to prevent unwanted side effects.
+ const steps = structuredClone(stepsInitial);
+
+ const parentStepPosition = findStepPositionOrThrow({
+ steps: steps,
+ stepId: parentStepId,
+ });
+
+ parentStepPosition.steps.splice(
+ parentStepPosition.index + 1, // The "+ 1" means that we add the step after its parent and not before.
+ 0,
+ stepToAdd,
+ );
+
+ return steps;
+};
diff --git a/packages/twenty-ui/src/display/icon/components/TablerIcons.ts b/packages/twenty-ui/src/display/icon/components/TablerIcons.ts
index aa95be3a50..b3423733b3 100644
--- a/packages/twenty-ui/src/display/icon/components/TablerIcons.ts
+++ b/packages/twenty-ui/src/display/icon/components/TablerIcons.ts
@@ -135,6 +135,7 @@ export {
IconPhoto,
IconPilcrow,
IconPlayerPlay,
+ IconPlaystationSquare,
IconPlug,
IconPlus,
IconPresentation,