From b84d1bc853f2da2c6e8a82487b173e267927d7f4 Mon Sep 17 00:00:00 2001 From: Devessier Date: Tue, 22 Oct 2024 17:48:21 +0200 Subject: [PATCH 01/12] feat: ensure every workflow trigger type is covered in enableTrigger and disableTrigger functions --- .../types/workflow-trigger.type.ts | 22 ++++++++++++++++++- .../workflow-trigger.workspace-service.ts | 18 ++++++++++----- packages/twenty-server/src/utils/assert.ts | 4 ++++ 3 files changed, 38 insertions(+), 6 deletions(-) diff --git a/packages/twenty-server/src/modules/workflow/workflow-trigger/types/workflow-trigger.type.ts b/packages/twenty-server/src/modules/workflow/workflow-trigger/types/workflow-trigger.type.ts index b2ca87bc9b88..dd7168ec5380 100644 --- a/packages/twenty-server/src/modules/workflow/workflow-trigger/types/workflow-trigger.type.ts +++ b/packages/twenty-server/src/modules/workflow/workflow-trigger/types/workflow-trigger.type.ts @@ -1,5 +1,6 @@ export enum WorkflowTriggerType { DATABASE_EVENT = 'DATABASE_EVENT', + MANUAL = 'MANUAL', } type BaseTrigger = { @@ -15,4 +16,23 @@ export type WorkflowDatabaseEventTrigger = BaseTrigger & { }; }; -export type WorkflowTrigger = WorkflowDatabaseEventTrigger; +export enum WorkflowManualTriggerAvaibility { + EVERYWHERE = 'EVERYWHERE', + WHEN_RECORD_SELECTED = 'WHEN_RECORD_SELECTED', +} + +export type WorkflowManualTrigger = BaseTrigger & { + type: WorkflowTriggerType.MANUAL; + settings: + | { + type: WorkflowManualTriggerAvaibility.EVERYWHERE; + } + | { + type: WorkflowManualTriggerAvaibility.WHEN_RECORD_SELECTED; + objectType: string; + }; +}; + +export type WorkflowTrigger = + | WorkflowDatabaseEventTrigger + | WorkflowManualTrigger; diff --git a/packages/twenty-server/src/modules/workflow/workflow-trigger/workspace-services/workflow-trigger.workspace-service.ts b/packages/twenty-server/src/modules/workflow/workflow-trigger/workspace-services/workflow-trigger.workspace-service.ts index 309f3d158772..125e457807d3 100644 --- a/packages/twenty-server/src/modules/workflow/workflow-trigger/workspace-services/workflow-trigger.workspace-service.ts +++ b/packages/twenty-server/src/modules/workflow/workflow-trigger/workspace-services/workflow-trigger.workspace-service.ts @@ -24,6 +24,7 @@ import { } from 'src/modules/workflow/workflow-trigger/exceptions/workflow-trigger.exception'; import { WorkflowTriggerType } from 'src/modules/workflow/workflow-trigger/types/workflow-trigger.type'; import { assertVersionCanBeActivated } from 'src/modules/workflow/workflow-trigger/utils/assert-version-can-be-activated.util'; +import { assertNever } from 'src/utils/assert'; @Injectable() export class WorkflowTriggerWorkspaceService { @@ -315,9 +316,13 @@ export class WorkflowTriggerWorkspaceService { workflowVersion.trigger, manager, ); - break; - default: - break; + + return; + case WorkflowTriggerType.MANUAL: + return; + default: { + assertNever(workflowVersion.trigger); + } } } @@ -333,9 +338,12 @@ export class WorkflowTriggerWorkspaceService { workflowVersion.workflowId, manager, ); - break; + + return; + case WorkflowTriggerType.MANUAL: + return; default: - break; + assertNever(workflowVersion.trigger); } } diff --git a/packages/twenty-server/src/utils/assert.ts b/packages/twenty-server/src/utils/assert.ts index 8ef03f2cb983..39092a9e4263 100644 --- a/packages/twenty-server/src/utils/assert.ts +++ b/packages/twenty-server/src/utils/assert.ts @@ -25,3 +25,7 @@ export const assert: Assert = (condition, message, ErrorType) => { export const assertNotNull = (item: T): item is NonNullable => item !== null && item !== undefined; + +export const assertNever = (_value: never, message?: string): never => { + throw new Error(message ?? "Didn't expect to get here."); +}; From 84c6bb4c56ec52181855314043b8e577d4475b20 Mon Sep 17 00:00:00 2001 From: Devessier Date: Wed, 23 Oct 2024 16:23:05 +0200 Subject: [PATCH 02/12] feat: wip --- .../components/RightDrawerRouter.tsx | 4 ++ .../constants/RightDrawerPageIcons.ts | 3 +- .../constants/RightDrawerPageTitles.ts | 3 +- .../right-drawer/types/RightDrawerPages.ts | 1 + .../RightDrawerWorkflowSelectTriggerType.tsx | 16 +++++ ...DrawerWorkflowSelectTriggerTypeContent.tsx | 60 +++++++++++++++++++ .../WorkflowDiagramCanvasEditableEffect.tsx | 7 +++ .../workflow/constants/TriggerTypes.ts | 19 ++++++ .../src/modules/workflow/types/Workflow.ts | 25 ++++++-- .../utils/getStepDefaultDefinition.ts | 3 +- .../utils/getTriggerDefaultDefinition.ts | 44 ++++++++++++++ 11 files changed, 178 insertions(+), 7 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 create mode 100644 packages/twenty-front/src/modules/workflow/constants/TriggerTypes.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 a5640cfc045e..820996bda1b4 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 85dc75ee18b6..64f594a5ff4b 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 9cba79382a0a..55e2f88995f9 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 68e20913a4f6..1ca51cb74484 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/RightDrawerWorkflowSelectTriggerType.tsx b/packages/twenty-front/src/modules/workflow/components/RightDrawerWorkflowSelectTriggerType.tsx new file mode 100644 index 000000000000..7eb10fc6ecb2 --- /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 000000000000..f2f53bced491 --- /dev/null +++ b/packages/twenty-front/src/modules/workflow/components/RightDrawerWorkflowSelectTriggerTypeContent.tsx @@ -0,0 +1,60 @@ +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 ad383a527b8c..9c9274366bcb 100644 --- a/packages/twenty-front/src/modules/workflow/components/WorkflowDiagramCanvasEditableEffect.tsx +++ b/packages/twenty-front/src/modules/workflow/components/WorkflowDiagramCanvasEditableEffect.tsx @@ -26,6 +26,13 @@ export const WorkflowDiagramCanvasEditableEffect = () => { return; } + const isEmptyTriggerNode = selectedNode.type === 'empty-trigger'; + if (isEmptyTriggerNode) { + openRightDrawer(RightDrawerPages.WorkflowStepSelectTriggerType); + + return; + } + const isCreateStepNode = selectedNode.type === 'create-step'; if (isCreateStepNode) { if (selectedNode.data.nodeType !== 'create-step') { diff --git a/packages/twenty-front/src/modules/workflow/constants/TriggerTypes.ts b/packages/twenty-front/src/modules/workflow/constants/TriggerTypes.ts new file mode 100644 index 000000000000..82da0b774f56 --- /dev/null +++ b/packages/twenty-front/src/modules/workflow/constants/TriggerTypes.ts @@ -0,0 +1,19 @@ +import { WorkflowTriggerType } from '@/workflow/types/Workflow'; +import { IconComponent, IconSettingsAutomation } from 'twenty-ui'; + +export const TRIGGER_TYPES: Array<{ + label: string; + type: WorkflowTriggerType; + icon: IconComponent; +}> = [ + { + label: 'Database Event', + type: 'DATABASE_EVENT', + icon: IconSettingsAutomation, + }, + { + label: 'Manual', + type: 'MANUAL', + icon: IconSettingsAutomation, + }, +]; diff --git a/packages/twenty-front/src/modules/workflow/types/Workflow.ts b/packages/twenty-front/src/modules/workflow/types/Workflow.ts index 70e3ab197020..57937f5df048 100644 --- a/packages/twenty-front/src/modules/workflow/types/Workflow.ts +++ b/packages/twenty-front/src/modules/workflow/types/Workflow.ts @@ -48,10 +48,8 @@ export type WorkflowActionType = WorkflowAction['type']; export type WorkflowStepType = WorkflowStep['type']; -export type WorkflowTriggerType = 'DATABASE_EVENT'; - type BaseTrigger = { - type: WorkflowTriggerType; + type: string; input?: object; }; @@ -62,7 +60,26 @@ export type WorkflowDatabaseEventTrigger = BaseTrigger & { }; }; -export type WorkflowTrigger = WorkflowDatabaseEventTrigger; +export type WorkflowManualTrigger = BaseTrigger & { + type: 'MANUAL'; + settings: + | { + type: 'EVERYWHERE'; + } + | { + type: 'WHEN_RECORD_SELECTED'; + objectType: string; + }; +}; + +export type WorkflowManualTriggerAvaibility = + WorkflowManualTrigger['settings']['type']; + +export type WorkflowTrigger = + | WorkflowDatabaseEventTrigger + | WorkflowManualTrigger; + +export type WorkflowTriggerType = WorkflowTrigger['type']; export type WorkflowStatus = 'DRAFT' | 'ACTIVE' | 'DEACTIVATED'; diff --git a/packages/twenty-front/src/modules/workflow/utils/getStepDefaultDefinition.ts b/packages/twenty-front/src/modules/workflow/utils/getStepDefaultDefinition.ts index fd63158d2548..d59a034865f1 100644 --- a/packages/twenty-front/src/modules/workflow/utils/getStepDefaultDefinition.ts +++ b/packages/twenty-front/src/modules/workflow/utils/getStepDefaultDefinition.ts @@ -1,4 +1,5 @@ import { WorkflowStep, WorkflowStepType } from '@/workflow/types/Workflow'; +import { assertUnreachable } from '@/workflow/utils/assertUnreachable'; import { v4 } from 'uuid'; export const getStepDefaultDefinition = ( @@ -53,7 +54,7 @@ export const getStepDefaultDefinition = ( }; } default: { - throw new Error(`Unknown type: ${type}`); + return assertUnreachable(type, `Unknown type: ${type}`); } } }; diff --git a/packages/twenty-front/src/modules/workflow/utils/getTriggerDefaultDefinition.ts b/packages/twenty-front/src/modules/workflow/utils/getTriggerDefaultDefinition.ts new file mode 100644 index 000000000000..cdab036b610b --- /dev/null +++ b/packages/twenty-front/src/modules/workflow/utils/getTriggerDefaultDefinition.ts @@ -0,0 +1,44 @@ +import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; +import { OBJECT_EVENT_TRIGGERS } from '@/workflow/constants/ObjectEventTriggers'; +import { + WorkflowTrigger, + WorkflowTriggerType, +} from '@/workflow/types/Workflow'; +import { assertUnreachable } from '@/workflow/utils/assertUnreachable'; + +export const getTriggerDefaultDefinition = ({ + type, + activeObjectMetadataItems, +}: { + type: WorkflowTriggerType; + activeObjectMetadataItems: ObjectMetadataItem[]; +}): WorkflowTrigger => { + if (activeObjectMetadataItems.length === 0) { + throw new Error( + 'This function need to receive at least one object metadata item to run.', + ); + } + + switch (type) { + case 'DATABASE_EVENT': { + return { + type, + settings: { + eventName: `${activeObjectMetadataItems[0].nameSingular}.${OBJECT_EVENT_TRIGGERS[0].value}`, + }, + }; + } + case 'MANUAL': { + return { + type, + settings: { + type: 'WHEN_RECORD_SELECTED', + objectType: activeObjectMetadataItems[0].nameSingular, + }, + }; + } + default: { + return assertUnreachable(type, `Unknown type: ${type}`); + } + } +}; From c72d1f4e1f4e431644f1ce4322429eed412d4447 Mon Sep 17 00:00:00 2001 From: Devessier Date: Wed, 23 Oct 2024 17:48:58 +0200 Subject: [PATCH 03/12] feat: create form for manual triggers and update flow node for triggers to support manual triggers --- .../WorkflowDiagramStepNodeBase.tsx | 34 ++-- ... WorkflowEditTriggerDatabaseEventForm.tsx} | 16 +- .../WorkflowEditTriggerManualForm.tsx | 149 ++++++++++++++++++ .../components/WorkflowStepDetail.tsx | 44 +++++- .../src/modules/workflow/types/Workflow.ts | 4 +- .../modules/workflow/types/WorkflowDiagram.ts | 12 +- .../workflow/utils/generateWorkflowDiagram.ts | 31 +++- .../utils/getManualTriggerDefaultSettings.ts | 30 ++++ .../utils/getTriggerDefaultDefinition.ts | 9 +- .../types/workflow-trigger.type.ts | 6 +- .../display/icon/components/TablerIcons.ts | 2 + 11 files changed, 299 insertions(+), 38 deletions(-) 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/utils/getManualTriggerDefaultSettings.ts diff --git a/packages/twenty-front/src/modules/workflow/components/WorkflowDiagramStepNodeBase.tsx b/packages/twenty-front/src/modules/workflow/components/WorkflowDiagramStepNodeBase.tsx index 0fc4d8591051..8a2f59da8e7a 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,14 +26,30 @@ export const WorkflowDiagramStepNodeBase = ({ const renderStepIcon = () => { switch (data.nodeType) { case 'trigger': { - return ( - - - - ); + switch (data.triggerType) { + case 'DATABASE_EVENT': { + return ( + + + + ); + } + case 'MANUAL': { + return ( + + + + ); + } + } + + return assertUnreachable(data.triggerType); } case 'condition': { return null; diff --git a/packages/twenty-front/src/modules/workflow/components/WorkflowEditTriggerForm.tsx b/packages/twenty-front/src/modules/workflow/components/WorkflowEditTriggerDatabaseEventForm.tsx similarity index 92% rename from packages/twenty-front/src/modules/workflow/components/WorkflowEditTriggerForm.tsx rename to packages/twenty-front/src/modules/workflow/components/WorkflowEditTriggerDatabaseEventForm.tsx index 6ff8d1a844d9..db45bae2f734 100644 --- a/packages/twenty-front/src/modules/workflow/components/WorkflowEditTriggerForm.tsx +++ b/packages/twenty-front/src/modules/workflow/components/WorkflowEditTriggerDatabaseEventForm.tsx @@ -1,7 +1,7 @@ import { useFilteredObjectMetadataItems } from '@/object-metadata/hooks/useFilteredObjectMetadataItems'; import { Select, SelectOption } from '@/ui/input/components/Select'; import { OBJECT_EVENT_TRIGGERS } from '@/workflow/constants/ObjectEventTriggers'; -import { WorkflowTrigger } from '@/workflow/types/Workflow'; +import { WorkflowDatabaseEventTrigger } from '@/workflow/types/Workflow'; import { splitWorkflowTriggerEventName } from '@/workflow/utils/splitWorkflowTriggerEventName'; import { useTheme } from '@emotion/react'; import styled from '@emotion/styled'; @@ -45,23 +45,23 @@ const StyledTriggerSettings = styled.div` row-gap: ${({ theme }) => theme.spacing(4)}; `; -type WorkflowEditTriggerFormProps = +type WorkflowEditTriggerDatabaseEventFormProps = | { - trigger: WorkflowTrigger | undefined; + trigger: WorkflowDatabaseEventTrigger; readonly: true; onTriggerUpdate?: undefined; } | { - trigger: WorkflowTrigger | undefined; + trigger: WorkflowDatabaseEventTrigger; readonly?: false; - onTriggerUpdate: (trigger: WorkflowTrigger) => void; + onTriggerUpdate: (trigger: WorkflowDatabaseEventTrigger) => void; }; -export const WorkflowEditTriggerForm = ({ +export const WorkflowEditTriggerDatabaseEventForm = ({ trigger, readonly, onTriggerUpdate, -}: WorkflowEditTriggerFormProps) => { +}: WorkflowEditTriggerDatabaseEventFormProps) => { const theme = useTheme(); const { activeObjectMetadataItems } = useFilteredObjectMetadataItems(); @@ -102,7 +102,7 @@ export const WorkflowEditTriggerForm = ({ {isDefined(selectedEvent) - ? `Trigger . Record is ${selectedEvent.label}` + ? `Trigger · Record is ${selectedEvent.label}` : '-'} diff --git a/packages/twenty-front/src/modules/workflow/components/WorkflowEditTriggerManualForm.tsx b/packages/twenty-front/src/modules/workflow/components/WorkflowEditTriggerManualForm.tsx new file mode 100644 index 000000000000..a9d69f9135cd --- /dev/null +++ b/packages/twenty-front/src/modules/workflow/components/WorkflowEditTriggerManualForm.tsx @@ -0,0 +1,149 @@ +import { useFilteredObjectMetadataItems } from '@/object-metadata/hooks/useFilteredObjectMetadataItems'; +import { Select, SelectOption } from '@/ui/input/components/Select'; +import { WorkflowManualTrigger } from '@/workflow/types/Workflow'; +import { getManualTriggerDefaultSettings } from '@/workflow/utils/getManualTriggerDefaultSettings'; +import { useTheme } from '@emotion/react'; +import styled from '@emotion/styled'; +import { useId } from 'react'; +import { IconCheckbox, IconHandMove, IconSquare } from 'twenty-ui'; + +const StyledTriggerHeader = styled.div` + background-color: ${({ theme }) => theme.background.secondary}; + border-bottom: 1px solid ${({ theme }) => theme.border.color.medium}; + display: flex; + flex-direction: column; + padding: ${({ theme }) => theme.spacing(6)}; +`; + +const StyledTriggerHeaderTitle = styled.p` + color: ${({ theme }) => theme.font.color.primary}; + font-weight: ${({ theme }) => theme.font.weight.semiBold}; + font-size: ${({ theme }) => theme.font.size.xl}; + + margin: ${({ theme }) => theme.spacing(3)} 0; +`; + +const StyledTriggerHeaderType = styled.p` + color: ${({ theme }) => theme.font.color.tertiary}; + margin: 0; +`; + +const StyledTriggerHeaderIconContainer = styled.div` + align-self: flex-start; + display: flex; + justify-content: center; + align-items: center; + background-color: ${({ theme }) => theme.background.transparent.light}; + border-radius: ${({ theme }) => theme.border.radius.xs}; + padding: ${({ theme }) => theme.spacing(1)}; +`; + +const StyledTriggerSettings = styled.div` + padding: ${({ theme }) => theme.spacing(6)}; + display: flex; + flex-direction: column; + row-gap: ${({ theme }) => theme.spacing(4)}; +`; + +type WorkflowEditTriggerManualFormProps = + | { + trigger: WorkflowManualTrigger; + readonly: true; + onTriggerUpdate?: undefined; + } + | { + trigger: WorkflowManualTrigger; + readonly?: false; + onTriggerUpdate: (trigger: WorkflowManualTrigger) => void; + }; + +export const WorkflowEditTriggerManualForm = ({ + trigger, + readonly, + onTriggerUpdate, +}: WorkflowEditTriggerManualFormProps) => { + const theme = useTheme(); + + const inputRootId = useId(); + + const { activeObjectMetadataItems } = useFilteredObjectMetadataItems(); + + const availableMetadata: Array> = + activeObjectMetadataItems.map((item) => ({ + label: item.labelPlural, + value: item.nameSingular, + })); + + return ( + <> + + + + + + Manual Trigger + + Trigger · Manual + + + + { + if (readonly === true) { + return; + } + + onTriggerUpdate({ + ...trigger, + settings: { + type: 'WHEN_RECORD_SELECTED', + objectType: updatedObject, + }, + }); + }} + /> + ) : null} + + + ); +}; diff --git a/packages/twenty-front/src/modules/workflow/components/WorkflowStepDetail.tsx b/packages/twenty-front/src/modules/workflow/components/WorkflowStepDetail.tsx index fa6af9f7a47f..b3e15092357e 100644 --- a/packages/twenty-front/src/modules/workflow/components/WorkflowStepDetail.tsx +++ b/packages/twenty-front/src/modules/workflow/components/WorkflowStepDetail.tsx @@ -1,6 +1,7 @@ import { WorkflowEditActionFormSendEmail } from '@/workflow/components/WorkflowEditActionFormSendEmail'; import { WorkflowEditActionFormServerlessFunction } from '@/workflow/components/WorkflowEditActionFormServerlessFunction'; -import { WorkflowEditTriggerForm } from '@/workflow/components/WorkflowEditTriggerForm'; +import { WorkflowEditTriggerDatabaseEventForm } from '@/workflow/components/WorkflowEditTriggerDatabaseEventForm'; +import { WorkflowEditTriggerManualForm } from '@/workflow/components/WorkflowEditTriggerManualForm'; import { WorkflowAction, WorkflowTrigger, @@ -41,12 +42,36 @@ export const WorkflowStepDetail = ({ switch (stepDefinition.type) { case 'trigger': { - return ( - + if (!isDefined(stepDefinition.definition)) { + throw new Error( + 'Expected the trigger to be defined at this point. Ensure the trigger has been set with a default value before trying to edit it.', + ); + } + + switch (stepDefinition.definition.type) { + case 'DATABASE_EVENT': { + return ( + + ); + } + case 'MANUAL': { + return ( + + ); + } + } + + return assertUnreachable( + stepDefinition.definition, + `Expected the step to have an handler; ${JSON.stringify(stepDefinition)}`, ); } case 'action': { @@ -70,6 +95,11 @@ export const WorkflowStepDetail = ({ ); } } + + return assertUnreachable( + stepDefinition.definition, + `Expected the step to have an handler; ${JSON.stringify(stepDefinition)}`, + ); } } diff --git a/packages/twenty-front/src/modules/workflow/types/Workflow.ts b/packages/twenty-front/src/modules/workflow/types/Workflow.ts index 57937f5df048..9786237de14a 100644 --- a/packages/twenty-front/src/modules/workflow/types/Workflow.ts +++ b/packages/twenty-front/src/modules/workflow/types/Workflow.ts @@ -72,7 +72,9 @@ export type WorkflowManualTrigger = BaseTrigger & { }; }; -export type WorkflowManualTriggerAvaibility = +export type WorkflowManualTriggerSettings = WorkflowManualTrigger['settings']; + +export type WorkflowManualTriggerAvailability = WorkflowManualTrigger['settings']['type']; export type WorkflowTrigger = diff --git a/packages/twenty-front/src/modules/workflow/types/WorkflowDiagram.ts b/packages/twenty-front/src/modules/workflow/types/WorkflowDiagram.ts index fdad5d19f785..ef3b2be2534f 100644 --- a/packages/twenty-front/src/modules/workflow/types/WorkflowDiagram.ts +++ b/packages/twenty-front/src/modules/workflow/types/WorkflowDiagram.ts @@ -1,4 +1,7 @@ -import { WorkflowActionType } from '@/workflow/types/Workflow'; +import { + WorkflowActionType, + WorkflowTriggerType, +} from '@/workflow/types/Workflow'; import { Edge, Node } from '@xyflow/react'; export type WorkflowDiagramNode = Node; @@ -11,7 +14,12 @@ export type WorkflowDiagram = { export type WorkflowDiagramStepNodeData = | { - nodeType: 'trigger' | 'condition'; + nodeType: 'condition'; + label: string; + } + | { + nodeType: 'trigger'; + triggerType: WorkflowTriggerType; label: string; } | { diff --git a/packages/twenty-front/src/modules/workflow/utils/generateWorkflowDiagram.ts b/packages/twenty-front/src/modules/workflow/utils/generateWorkflowDiagram.ts index ff5a211e64dd..971e996684f3 100644 --- a/packages/twenty-front/src/modules/workflow/utils/generateWorkflowDiagram.ts +++ b/packages/twenty-front/src/modules/workflow/utils/generateWorkflowDiagram.ts @@ -5,6 +5,7 @@ import { WorkflowDiagramEdge, WorkflowDiagramNode, } from '@/workflow/types/WorkflowDiagram'; +import { assertUnreachable } from '@/workflow/utils/assertUnreachable'; import { splitWorkflowTriggerEventName } from '@/workflow/utils/splitWorkflowTriggerEventName'; import { MarkerType } from '@xyflow/react'; import { isDefined } from 'twenty-ui'; @@ -59,15 +60,37 @@ export const generateWorkflowDiagram = ({ const triggerNodeId = TRIGGER_STEP_ID; if (isDefined(trigger)) { - const triggerEvent = splitWorkflowTriggerEventName( - trigger.settings.eventName, - ); + let triggerLabel: string; + + switch (trigger.type) { + case 'MANUAL': { + triggerLabel = 'Manual Trigger'; + + break; + } + case 'DATABASE_EVENT': { + const triggerEvent = splitWorkflowTriggerEventName( + trigger.settings.eventName, + ); + + triggerLabel = `${capitalize(triggerEvent.objectType)} is ${capitalize(triggerEvent.event)}`; + + break; + } + default: { + return assertUnreachable( + trigger, + `Expected the trigger "${JSON.stringify(trigger)}" to be supported.`, + ); + } + } nodes.push({ id: triggerNodeId, data: { nodeType: 'trigger', - label: `${capitalize(triggerEvent.objectType)} is ${capitalize(triggerEvent.event)}`, + triggerType: trigger.type, + label: triggerLabel, }, position: { x: 0, diff --git a/packages/twenty-front/src/modules/workflow/utils/getManualTriggerDefaultSettings.ts b/packages/twenty-front/src/modules/workflow/utils/getManualTriggerDefaultSettings.ts new file mode 100644 index 000000000000..b53e81ec6edf --- /dev/null +++ b/packages/twenty-front/src/modules/workflow/utils/getManualTriggerDefaultSettings.ts @@ -0,0 +1,30 @@ +import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; +import { + WorkflowManualTriggerAvailability, + WorkflowManualTriggerSettings, +} from '@/workflow/types/Workflow'; +import { assertUnreachable } from '@/workflow/utils/assertUnreachable'; + +export const getManualTriggerDefaultSettings = ({ + availability, + activeObjectMetadataItems, +}: { + availability: WorkflowManualTriggerAvailability; + activeObjectMetadataItems: ObjectMetadataItem[]; +}): WorkflowManualTriggerSettings => { + switch (availability) { + case 'EVERYWHERE': { + return { + type: 'EVERYWHERE', + }; + } + case 'WHEN_RECORD_SELECTED': { + return { + type: 'WHEN_RECORD_SELECTED', + objectType: activeObjectMetadataItems[0].nameSingular, + }; + } + } + + return assertUnreachable(availability); +}; diff --git a/packages/twenty-front/src/modules/workflow/utils/getTriggerDefaultDefinition.ts b/packages/twenty-front/src/modules/workflow/utils/getTriggerDefaultDefinition.ts index cdab036b610b..dc0a762449cc 100644 --- a/packages/twenty-front/src/modules/workflow/utils/getTriggerDefaultDefinition.ts +++ b/packages/twenty-front/src/modules/workflow/utils/getTriggerDefaultDefinition.ts @@ -5,6 +5,7 @@ import { WorkflowTriggerType, } from '@/workflow/types/Workflow'; import { assertUnreachable } from '@/workflow/utils/assertUnreachable'; +import { getManualTriggerDefaultSettings } from '@/workflow/utils/getManualTriggerDefaultSettings'; export const getTriggerDefaultDefinition = ({ type, @@ -31,10 +32,10 @@ export const getTriggerDefaultDefinition = ({ case 'MANUAL': { return { type, - settings: { - type: 'WHEN_RECORD_SELECTED', - objectType: activeObjectMetadataItems[0].nameSingular, - }, + settings: getManualTriggerDefaultSettings({ + availability: 'WHEN_RECORD_SELECTED', + activeObjectMetadataItems, + }), }; } default: { diff --git a/packages/twenty-server/src/modules/workflow/workflow-trigger/types/workflow-trigger.type.ts b/packages/twenty-server/src/modules/workflow/workflow-trigger/types/workflow-trigger.type.ts index dd7168ec5380..0a80ea4de6cb 100644 --- a/packages/twenty-server/src/modules/workflow/workflow-trigger/types/workflow-trigger.type.ts +++ b/packages/twenty-server/src/modules/workflow/workflow-trigger/types/workflow-trigger.type.ts @@ -16,7 +16,7 @@ export type WorkflowDatabaseEventTrigger = BaseTrigger & { }; }; -export enum WorkflowManualTriggerAvaibility { +export enum WorkflowManualTriggerAvailability { EVERYWHERE = 'EVERYWHERE', WHEN_RECORD_SELECTED = 'WHEN_RECORD_SELECTED', } @@ -25,10 +25,10 @@ export type WorkflowManualTrigger = BaseTrigger & { type: WorkflowTriggerType.MANUAL; settings: | { - type: WorkflowManualTriggerAvaibility.EVERYWHERE; + type: WorkflowManualTriggerAvailability.EVERYWHERE; } | { - type: WorkflowManualTriggerAvaibility.WHEN_RECORD_SELECTED; + type: WorkflowManualTriggerAvailability.WHEN_RECORD_SELECTED; objectType: string; }; }; diff --git a/packages/twenty-ui/src/display/icon/components/TablerIcons.ts b/packages/twenty-ui/src/display/icon/components/TablerIcons.ts index 76bd4bc2a4a5..efe408862975 100644 --- a/packages/twenty-ui/src/display/icon/components/TablerIcons.ts +++ b/packages/twenty-ui/src/display/icon/components/TablerIcons.ts @@ -179,6 +179,8 @@ export { IconPlayerPlay, IconPlayerStop, IconPlaylistAdd, + IconHandMove, + IconSquare, IconPlaystationSquare, IconPlug, IconPlus, From 097eaea9ef0291c1c68c9e72fbbeed3b9e689f87 Mon Sep 17 00:00:00 2001 From: Devessier Date: Wed, 23 Oct 2024 17:49:22 +0200 Subject: [PATCH 04/12] refactor: unify how we define forms for actions and triggers --- .../WorkflowEditActionFormSendEmail.tsx | 170 +++++++++--------- ...rkflowEditActionFormServerlessFunction.tsx | 64 +++---- ...se.tsx => WorkflowEditGenericFormBase.tsx} | 43 +++-- .../WorkflowEditTriggerManualForm.tsx | 148 ++++++--------- 4 files changed, 182 insertions(+), 243 deletions(-) rename packages/twenty-front/src/modules/workflow/components/{WorkflowEditActionFormBase.tsx => WorkflowEditGenericFormBase.tsx} (54%) diff --git a/packages/twenty-front/src/modules/workflow/components/WorkflowEditActionFormSendEmail.tsx b/packages/twenty-front/src/modules/workflow/components/WorkflowEditActionFormSendEmail.tsx index 764da25a815b..bbe69aa4bae0 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" > - - ( -