From 0144553667d03404865c35e13adfe8af379b9c90 Mon Sep 17 00:00:00 2001 From: Baptiste Devessier Date: Fri, 25 Oct 2024 14:24:56 +0200 Subject: [PATCH] Add Manual Triggers (#8024) In this PR: - Add support for manual triggers in the backend - Add a right drawer to let users select the type of trigger they want - Create a specific right drawer for database event triggers - Create a right drawer for manual triggers; let the user select where the manual trigger should be made available - Create a default trigger as soon as the user selects the type of trigger they want. It prevents the user to see empty selects for record type and event type. By default, the database event trigger will be set to "company.created". It should be visible enough for users to understand what happens and choose another record type or event type. https://github.com/user-attachments/assets/29a21985-1823-4890-9eb3-e4f876459c7a --- .../components/RightDrawerRouter.tsx | 4 + .../constants/RightDrawerPageIcons.ts | 3 +- .../constants/RightDrawerPageTitles.ts | 3 +- .../right-drawer/types/RightDrawerPages.ts | 1 + ...RightDrawerWorkflowSelectActionContent.tsx | 24 ++- .../RightDrawerWorkflowSelectTriggerType.tsx | 16 ++ ...DrawerWorkflowSelectTriggerTypeContent.tsx | 58 ++++++ .../WorkflowDiagramCanvasEditableEffect.tsx | 11 +- .../WorkflowDiagramStepNodeBase.tsx | 37 ++-- .../WorkflowEditActionFormSendEmail.tsx | 168 ++++++++---------- ...rkflowEditActionFormServerlessFunction.tsx | 64 +++---- ...se.tsx => WorkflowEditGenericFormBase.tsx} | 43 +++-- ... WorkflowEditTriggerDatabaseEventForm.tsx} | 16 +- .../WorkflowEditTriggerManualForm.tsx | 97 ++++++++++ .../components/WorkflowStepDetail.tsx | 44 ++++- .../workflow/constants/CreateStepStepId.ts | 1 + .../workflow/constants/EmptyTriggerStepId.ts | 1 + .../ManualTriggerAvailabilityOptions.ts | 19 ++ .../workflow/constants/TriggerTypes.ts | 19 ++ .../src/modules/workflow/types/Workflow.ts | 23 ++- .../modules/workflow/types/WorkflowDiagram.ts | 8 +- .../getWorkflowVersionDiagram.test.ts | 1 + .../workflow/utils/generateWorkflowDiagram.ts | 31 +++- .../utils/getManualTriggerDefaultSettings.ts | 29 +++ .../utils/getStepDefaultDefinition.ts | 3 +- .../utils/getTriggerDefaultDefinition.ts | 45 +++++ .../types/workflow-trigger.type.ts | 19 +- .../assert-version-can-be-activated.util.ts | 2 + .../workflow-trigger.workspace-service.ts | 18 +- packages/twenty-server/src/utils/assert.ts | 4 + .../display/icon/components/TablerIcons.ts | 2 + 31 files changed, 609 insertions(+), 205 deletions(-) create mode 100644 packages/twenty-front/src/modules/workflow/components/RightDrawerWorkflowSelectTriggerType.tsx create mode 100644 packages/twenty-front/src/modules/workflow/components/RightDrawerWorkflowSelectTriggerTypeContent.tsx rename packages/twenty-front/src/modules/workflow/components/{WorkflowEditActionFormBase.tsx => WorkflowEditGenericFormBase.tsx} (54%) rename packages/twenty-front/src/modules/workflow/components/{WorkflowEditTriggerForm.tsx => WorkflowEditTriggerDatabaseEventForm.tsx} (92%) create mode 100644 packages/twenty-front/src/modules/workflow/components/WorkflowEditTriggerManualForm.tsx create mode 100644 packages/twenty-front/src/modules/workflow/constants/CreateStepStepId.ts create mode 100644 packages/twenty-front/src/modules/workflow/constants/EmptyTriggerStepId.ts create mode 100644 packages/twenty-front/src/modules/workflow/constants/ManualTriggerAvailabilityOptions.ts create mode 100644 packages/twenty-front/src/modules/workflow/constants/TriggerTypes.ts create mode 100644 packages/twenty-front/src/modules/workflow/utils/getManualTriggerDefaultSettings.ts create mode 100644 packages/twenty-front/src/modules/workflow/utils/getTriggerDefaultDefinition.ts 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 a5640cfc0..820996bda 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 @@ -15,6 +15,7 @@ import { RightDrawerWorkflowViewStep } from '@/workflow/components/RightDrawerWo import { isDefined } from 'twenty-ui'; import { rightDrawerPageState } from '../states/rightDrawerPageState'; import { RightDrawerPages } from '../types/RightDrawerPages'; +import { RightDrawerWorkflowSelectTriggerType } from '@/workflow/components/RightDrawerWorkflowSelectTriggerType'; const StyledRightDrawerPage = styled.div` display: flex; @@ -38,6 +39,9 @@ const RIGHT_DRAWER_PAGES_CONFIG: ComponentByRightDrawerPage = { [RightDrawerPages.ViewCalendarEvent]: , [RightDrawerPages.ViewRecord]: , [RightDrawerPages.Copilot]: , + [RightDrawerPages.WorkflowStepSelectTriggerType]: ( + + ), [RightDrawerPages.WorkflowStepSelectAction]: ( ), 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 85dc75ee1..64f594a5f 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,7 +5,8 @@ export const RIGHT_DRAWER_PAGE_ICONS = { [RightDrawerPages.ViewCalendarEvent]: 'IconCalendarEvent', [RightDrawerPages.ViewRecord]: 'Icon123', [RightDrawerPages.Copilot]: 'IconSparkles', - [RightDrawerPages.WorkflowStepEdit]: 'IconSparkles', + [RightDrawerPages.WorkflowStepSelectTriggerType]: 'IconSparkles', [RightDrawerPages.WorkflowStepSelectAction]: 'IconSparkles', + [RightDrawerPages.WorkflowStepEdit]: 'IconSparkles', [RightDrawerPages.WorkflowStepView]: '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 9cba79382..55e2f8899 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,7 +5,8 @@ export const RIGHT_DRAWER_PAGE_TITLES = { [RightDrawerPages.ViewCalendarEvent]: 'Calendar Event', [RightDrawerPages.ViewRecord]: 'Record Editor', [RightDrawerPages.Copilot]: 'Copilot', - [RightDrawerPages.WorkflowStepEdit]: 'Workflow', + [RightDrawerPages.WorkflowStepSelectTriggerType]: 'Workflow', [RightDrawerPages.WorkflowStepSelectAction]: 'Workflow', + [RightDrawerPages.WorkflowStepEdit]: 'Workflow', [RightDrawerPages.WorkflowStepView]: '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 68e20913a..1ca51cb74 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,6 +3,7 @@ export enum RightDrawerPages { ViewCalendarEvent = 'view-calendar-event', ViewRecord = 'view-record', Copilot = 'copilot', + WorkflowStepSelectTriggerType = 'workflow-step-select-trigger-type', WorkflowStepSelectAction = 'workflow-step-select-action', WorkflowStepView = 'workflow-step-view', WorkflowStepEdit = 'workflow-step-edit', diff --git a/packages/twenty-front/src/modules/workflow/components/RightDrawerWorkflowSelectActionContent.tsx b/packages/twenty-front/src/modules/workflow/components/RightDrawerWorkflowSelectActionContent.tsx index 094cc99e0..c3ac8483c 100644 --- a/packages/twenty-front/src/modules/workflow/components/RightDrawerWorkflowSelectActionContent.tsx +++ b/packages/twenty-front/src/modules/workflow/components/RightDrawerWorkflowSelectActionContent.tsx @@ -24,18 +24,16 @@ export const RightDrawerWorkflowSelectActionContent = ({ }); return ( - <> - - {ACTIONS.map((action) => ( - { - return createStep(action.type); - }} - /> - ))} - - + + {ACTIONS.map((action) => ( + { + return createStep(action.type); + }} + /> + ))} + ); }; diff --git a/packages/twenty-front/src/modules/workflow/components/RightDrawerWorkflowSelectTriggerType.tsx b/packages/twenty-front/src/modules/workflow/components/RightDrawerWorkflowSelectTriggerType.tsx new file mode 100644 index 000000000..7eb10fc6e --- /dev/null +++ b/packages/twenty-front/src/modules/workflow/components/RightDrawerWorkflowSelectTriggerType.tsx @@ -0,0 +1,16 @@ +import { RightDrawerWorkflowSelectTriggerTypeContent } from '@/workflow/components/RightDrawerWorkflowSelectTriggerTypeContent'; +import { useWorkflowWithCurrentVersion } from '@/workflow/hooks/useWorkflowWithCurrentVersion'; +import { workflowIdState } from '@/workflow/states/workflowIdState'; +import { useRecoilValue } from 'recoil'; +import { isDefined } from 'twenty-ui'; + +export const RightDrawerWorkflowSelectTriggerType = () => { + const workflowId = useRecoilValue(workflowIdState); + const workflow = useWorkflowWithCurrentVersion(workflowId); + + if (!isDefined(workflow)) { + return null; + } + + return ; +}; diff --git a/packages/twenty-front/src/modules/workflow/components/RightDrawerWorkflowSelectTriggerTypeContent.tsx b/packages/twenty-front/src/modules/workflow/components/RightDrawerWorkflowSelectTriggerTypeContent.tsx new file mode 100644 index 000000000..daf169c27 --- /dev/null +++ b/packages/twenty-front/src/modules/workflow/components/RightDrawerWorkflowSelectTriggerTypeContent.tsx @@ -0,0 +1,58 @@ +import { useFilteredObjectMetadataItems } from '@/object-metadata/hooks/useFilteredObjectMetadataItems'; +import { useRightDrawer } from '@/ui/layout/right-drawer/hooks/useRightDrawer'; +import { RightDrawerPages } from '@/ui/layout/right-drawer/types/RightDrawerPages'; +import { MenuItem } from '@/ui/navigation/menu-item/components/MenuItem'; +import { TRIGGER_STEP_ID } from '@/workflow/constants/TriggerStepId'; +import { TRIGGER_TYPES } from '@/workflow/constants/TriggerTypes'; +import { useUpdateWorkflowVersionTrigger } from '@/workflow/hooks/useUpdateWorkflowVersionTrigger'; +import { workflowSelectedNodeState } from '@/workflow/states/workflowSelectedNodeState'; +import { WorkflowWithCurrentVersion } from '@/workflow/types/Workflow'; +import { getTriggerDefaultDefinition } from '@/workflow/utils/getTriggerDefaultDefinition'; +import styled from '@emotion/styled'; +import { useSetRecoilState } from 'recoil'; + +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 RightDrawerWorkflowSelectTriggerTypeContent = ({ + workflow, +}: { + workflow: WorkflowWithCurrentVersion; +}) => { + const { updateTrigger } = useUpdateWorkflowVersionTrigger({ workflow }); + + const { activeObjectMetadataItems } = useFilteredObjectMetadataItems(); + + const { openRightDrawer } = useRightDrawer(); + const setWorkflowSelectedNode = useSetRecoilState(workflowSelectedNodeState); + + return ( + + {TRIGGER_TYPES.map((action) => ( + { + await updateTrigger( + getTriggerDefaultDefinition({ + type: action.type, + activeObjectMetadataItems, + }), + ); + + setWorkflowSelectedNode(TRIGGER_STEP_ID); + + openRightDrawer(RightDrawerPages.WorkflowStepEdit); + }} + /> + ))} + + ); +}; diff --git a/packages/twenty-front/src/modules/workflow/components/WorkflowDiagramCanvasEditableEffect.tsx b/packages/twenty-front/src/modules/workflow/components/WorkflowDiagramCanvasEditableEffect.tsx index ad383a527..64f3a723f 100644 --- a/packages/twenty-front/src/modules/workflow/components/WorkflowDiagramCanvasEditableEffect.tsx +++ b/packages/twenty-front/src/modules/workflow/components/WorkflowDiagramCanvasEditableEffect.tsx @@ -1,5 +1,7 @@ import { useRightDrawer } from '@/ui/layout/right-drawer/hooks/useRightDrawer'; import { RightDrawerPages } from '@/ui/layout/right-drawer/types/RightDrawerPages'; +import { CREATE_STEP_STEP_ID } from '@/workflow/constants/CreateStepStepId'; +import { EMPTY_TRIGGER_STEP_ID } from '@/workflow/constants/EmptyTriggerStepId'; import { useStartNodeCreation } from '@/workflow/hooks/useStartNodeCreation'; import { useTriggerNodeSelection } from '@/workflow/hooks/useTriggerNodeSelection'; import { workflowSelectedNodeState } from '@/workflow/states/workflowSelectedNodeState'; @@ -26,7 +28,14 @@ export const WorkflowDiagramCanvasEditableEffect = () => { return; } - const isCreateStepNode = selectedNode.type === 'create-step'; + const isEmptyTriggerNode = selectedNode.type === EMPTY_TRIGGER_STEP_ID; + if (isEmptyTriggerNode) { + openRightDrawer(RightDrawerPages.WorkflowStepSelectTriggerType); + + return; + } + + const isCreateStepNode = selectedNode.type === CREATE_STEP_STEP_ID; if (isCreateStepNode) { if (selectedNode.data.nodeType !== 'create-step') { throw new Error('Expected selected node to be a create step node.'); diff --git a/packages/twenty-front/src/modules/workflow/components/WorkflowDiagramStepNodeBase.tsx b/packages/twenty-front/src/modules/workflow/components/WorkflowDiagramStepNodeBase.tsx index 0fc4d8591..9cef1fbb4 100644 --- a/packages/twenty-front/src/modules/workflow/components/WorkflowDiagramStepNodeBase.tsx +++ b/packages/twenty-front/src/modules/workflow/components/WorkflowDiagramStepNodeBase.tsx @@ -3,7 +3,7 @@ import { WorkflowDiagramStepNodeData } from '@/workflow/types/WorkflowDiagram'; import { assertUnreachable } from '@/workflow/utils/assertUnreachable'; import { useTheme } from '@emotion/react'; import styled from '@emotion/styled'; -import { IconCode, IconMail, IconPlaylistAdd } from 'twenty-ui'; +import { IconCode, IconHandMove, IconMail, IconPlaylistAdd } from 'twenty-ui'; const StyledStepNodeLabelIconContainer = styled.div` align-items: center; @@ -26,17 +26,30 @@ export const WorkflowDiagramStepNodeBase = ({ const renderStepIcon = () => { switch (data.nodeType) { case 'trigger': { - return ( - - - - ); - } - case 'condition': { - return null; + switch (data.triggerType) { + case 'DATABASE_EVENT': { + return ( + + + + ); + } + case 'MANUAL': { + return ( + + + + ); + } + } + + return assertUnreachable(data.triggerType); } case 'action': { switch (data.actionType) { diff --git a/packages/twenty-front/src/modules/workflow/components/WorkflowEditActionFormSendEmail.tsx b/packages/twenty-front/src/modules/workflow/components/WorkflowEditActionFormSendEmail.tsx index 764da25a8..bbe69aa4b 100644 --- a/packages/twenty-front/src/modules/workflow/components/WorkflowEditActionFormSendEmail.tsx +++ b/packages/twenty-front/src/modules/workflow/components/WorkflowEditActionFormSendEmail.tsx @@ -5,25 +5,17 @@ import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords'; import { useTriggerGoogleApisOAuth } from '@/settings/accounts/hooks/useTriggerGoogleApisOAuth'; import { Select, SelectOption } from '@/ui/input/components/Select'; import { TextArea } from '@/ui/input/components/TextArea'; -import { WorkflowEditActionFormBase } from '@/workflow/components/WorkflowEditActionFormBase'; -import { VariableTagInput } from '@/workflow/search-variables/components/VariableTagInput'; +import { WorkflowEditGenericFormBase } from '@/workflow/components/WorkflowEditGenericFormBase'; +import VariableTagInput from '@/workflow/search-variables/components/VariableTagInput'; import { workflowIdState } from '@/workflow/states/workflowIdState'; import { WorkflowSendEmailStep } from '@/workflow/types/Workflow'; import { useTheme } from '@emotion/react'; -import styled from '@emotion/styled'; import { useEffect } from 'react'; import { Controller, useForm } from 'react-hook-form'; import { useRecoilValue } from 'recoil'; import { IconMail, IconPlus, isDefined } from 'twenty-ui'; import { useDebouncedCallback } from 'use-debounce'; -const StyledTriggerSettings = styled.div` - padding: ${({ theme }) => theme.spacing(6)}; - display: flex; - flex-direction: column; - row-gap: ${({ theme }) => theme.spacing(4)}; -`; - type WorkflowEditActionFormSendEmailProps = | { action: WorkflowSendEmailStep; @@ -174,87 +166,85 @@ export const WorkflowEditActionFormSendEmail = ( return ( !loading && ( - } - actionTitle="Send Email" - actionType="Email" + } + headerTitle="Send Email" + headerType="Email" > - - ( - + triggerGoogleApisOAuth({ redirectLocation: redirectUrl }), + Icon: IconPlus, + text: 'Add account', + }} + onChange={(connectedAccountId) => { + field.onChange(connectedAccountId); + handleSave(true); + }} + /> + )} + /> + ( + { + field.onChange(email); + handleSave(); + }} + /> + )} + /> + ( + { + field.onChange(email); + handleSave(); + }} + /> + )} + /> - ( -