From f7c99ddc7ad5a7b49372868f743cdb9fdf1bda78 Mon Sep 17 00:00:00 2001 From: Baptiste Devessier Date: Fri, 30 Aug 2024 15:51:36 +0200 Subject: [PATCH] Create new steps in workflow editor (#6764) This PR adds the possibility of creating new steps. For now, only actions are available. The steps are stored on the server, and the visualizer is reloaded to include them. Selecting a step opens the right drawer and shows its details. For now, it's only the id of the step, but in the future, it will be the parameters of the step. In the future we'll want to let users add steps at any point in the diagram. As a consequence, it's crucial to be able to walk in the tree that make the steps to find the correct place where to put the new step. I wrote a function that returns where the new step should be inserted. This function will become recursive once we get branching implemented. Things to mention: - Reactflow needs every node and edge to have a unique identifier. In this PR, I chose to use steps' id as nodes' id. That way, it's easy to move from a node to a step, which helps make operations on a step without resolving the step's id from the node's id. --- packages/twenty-front/setupTests.ts | 11 + ...sePageChangeEffectNavigateLocation.test.ts | 11 + .../types/CoreObjectNameSingular.ts | 1 + .../hooks/__tests__/useShowAuthModal.test.tsx | 11 + .../components/RightDrawerRouter.tsx | 8 +- .../constants/RightDrawerPageIcons.ts | 3 +- .../constants/RightDrawerPageTitles.ts | 3 +- .../right-drawer/types/RightDrawerPages.ts | 3 +- .../components/RightDrawerWorkflow.tsx | 40 ---- .../RightDrawerWorkflowEditStep.tsx | 10 + .../RightDrawerWorkflowSelectAction.tsx | 28 +++ ...RightDrawerWorkflowSelectActionContent.tsx | 60 +++++ .../components/WorkflowShowPageDiagram.tsx | 5 +- ...WorkflowShowPageDiagramCreateStepNode.tsx} | 14 +- .../WorkflowShowPageDiagramEffect.tsx | 81 +++++++ .../components/WorkflowShowPageEffect.tsx | 6 + .../modules/workflow/hooks/useCreateNode.tsx | 47 ++++ .../useRightDrawerWorkflowSelectAction.tsx | 117 ++++++++++ .../workflow/hooks/useStartNodeCreation.tsx | 29 +++ ...orkflowDiagramTriggerNodeSelectionState.ts | 8 + .../states/showPageWorkflowIdState.ts | 6 + .../showPageWorkflowSelectedNodeState.ts | 8 + ...workflowCreateStepFromParentStepIdState.ts | 8 + .../modules/workflow/types/WorkflowDiagram.ts | 5 +- .../__tests__/generateWorkflowDiagram.test.ts | 6 +- .../utils/__tests__/insertStep.test.ts | 217 ++++++++++++++++++ .../workflow/utils/addCreateStepNodes.ts | 5 +- .../workflow/utils/generateWorkflowDiagram.ts | 4 +- .../workflow/utils/getOrganizedDiagram.ts | 3 - .../utils/getWorkflowLastDiagramVersion.ts | 3 +- .../workflow/utils/getWorkflowLastVersion.ts | 10 + .../src/modules/workflow/utils/insertStep.ts | 61 +++++ .../display/icon/components/TablerIcons.ts | 1 + 33 files changed, 766 insertions(+), 67 deletions(-) delete mode 100644 packages/twenty-front/src/modules/workflow/components/RightDrawerWorkflow.tsx create mode 100644 packages/twenty-front/src/modules/workflow/components/RightDrawerWorkflowEditStep.tsx create mode 100644 packages/twenty-front/src/modules/workflow/components/RightDrawerWorkflowSelectAction.tsx create mode 100644 packages/twenty-front/src/modules/workflow/components/RightDrawerWorkflowSelectActionContent.tsx rename packages/twenty-front/src/modules/workflow/components/{WorkflowShowPageDiagramCreateStepNode.tsx.tsx => WorkflowShowPageDiagramCreateStepNode.tsx} (50%) create mode 100644 packages/twenty-front/src/modules/workflow/components/WorkflowShowPageDiagramEffect.tsx create mode 100644 packages/twenty-front/src/modules/workflow/hooks/useCreateNode.tsx create mode 100644 packages/twenty-front/src/modules/workflow/hooks/useRightDrawerWorkflowSelectAction.tsx create mode 100644 packages/twenty-front/src/modules/workflow/hooks/useStartNodeCreation.tsx create mode 100644 packages/twenty-front/src/modules/workflow/states/showPageWorkflowDiagramTriggerNodeSelectionState.ts create mode 100644 packages/twenty-front/src/modules/workflow/states/showPageWorkflowIdState.ts create mode 100644 packages/twenty-front/src/modules/workflow/states/showPageWorkflowSelectedNodeState.ts create mode 100644 packages/twenty-front/src/modules/workflow/states/workflowCreateStepFromParentStepIdState.ts create mode 100644 packages/twenty-front/src/modules/workflow/utils/__tests__/insertStep.test.ts create mode 100644 packages/twenty-front/src/modules/workflow/utils/getWorkflowLastVersion.ts create mode 100644 packages/twenty-front/src/modules/workflow/utils/insertStep.ts diff --git a/packages/twenty-front/setupTests.ts b/packages/twenty-front/setupTests.ts index 8f2609b7b3e0..e5aa41c46c9f 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 95c2a58b79c6..bde6abe9c771 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 6e0d13f48ddf..dd496e70d3d2 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 ee0a46590417..e81f8701e8e2 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 bf5ab29bebf6..972b2a75e3a2 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 aed035783839..7fc5d9849d89 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 bb74c9da8153..749fb10384fc 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 2217d437f4fd..f016669b48a2 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 b12e749a899e..000000000000 --- 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 000000000000..96b82e2f003c --- /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 000000000000..d99189f66fcf --- /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 000000000000..bc888238b2ef --- /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) => ( + { + handleActionClick(option.id); + }} + /> + ))} + + + ); +}; diff --git a/packages/twenty-front/src/modules/workflow/components/WorkflowShowPageDiagram.tsx b/packages/twenty-front/src/modules/workflow/components/WorkflowShowPageDiagram.tsx index 090d074ad131..9643aaa7a045 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 a799480e81ff..95fb6e836682 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 000000000000..227d66a8d599 --- /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 6d41e5d7fd4b..56d27ffb7004 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 000000000000..c4873c9a3313 --- /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 000000000000..5284de94ddb9 --- /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 000000000000..54e479cfb604 --- /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 000000000000..780b57f3cb2d --- /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 000000000000..a481713abdd7 --- /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 000000000000..616c61c6e817 --- /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 000000000000..4e64010b9630 --- /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 f97d5027ed0c..237daab24b93 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 0716aa29bb83..885393576536 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 000000000000..4479abe6f900 --- /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 9d2941adb274..381d05889874 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 6bb95f23cbba..6399bb995b77 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 0aa687d71054..a5d549c71ad6 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 448ed7fec4b6..7e4d13a693dc 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 000000000000..b69790e9c469 --- /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 000000000000..038402e789fe --- /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 aa95be3a50c6..b3423733b341 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,