diff --git a/packages/twenty-front/src/modules/workflow/components/RightDrawerWorkflowEditStepContent.tsx b/packages/twenty-front/src/modules/workflow/components/RightDrawerWorkflowEditStepContent.tsx index a5669f989878..9fb36225678c 100644 --- a/packages/twenty-front/src/modules/workflow/components/RightDrawerWorkflowEditStepContent.tsx +++ b/packages/twenty-front/src/modules/workflow/components/RightDrawerWorkflowEditStepContent.tsx @@ -23,7 +23,10 @@ const getStepDefinitionOrThrow = ({ if (stepId === TRIGGER_STEP_ID) { if (!isDefined(currentVersion.trigger)) { - throw new Error('Expected to find the definition of the trigger'); + return { + type: 'trigger', + definition: undefined, + } as const; } return { @@ -33,7 +36,9 @@ const getStepDefinitionOrThrow = ({ } if (!isDefined(currentVersion.steps)) { - throw new Error('Expected to find an array of steps'); + throw new Error( + 'Malformed workflow version: missing steps information; be sure to create at least one step before trying to edit one', + ); } const selectedNodePosition = findStepPositionOrThrow({ @@ -74,7 +79,7 @@ export const RightDrawerWorkflowEditStepContent = ({ return ( ); } @@ -82,7 +87,7 @@ export const RightDrawerWorkflowEditStepContent = ({ return ( ); }; diff --git a/packages/twenty-front/src/modules/workflow/components/Workflow.tsx b/packages/twenty-front/src/modules/workflow/components/Workflow.tsx index d0b3331e045c..7c7e5bc74b62 100644 --- a/packages/twenty-front/src/modules/workflow/components/Workflow.tsx +++ b/packages/twenty-front/src/modules/workflow/components/Workflow.tsx @@ -1,6 +1,6 @@ import { ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity'; import { WorkflowDiagramCanvas } from '@/workflow/components/WorkflowDiagramCanvas'; -import { WorkflowShowPageEffect } from '@/workflow/components/WorkflowShowPageEffect'; +import { WorkflowEffect } from '@/workflow/components/WorkflowEffect'; import { workflowDiagramState } from '@/workflow/states/workflowDiagramState'; import styled from '@emotion/styled'; import '@xyflow/react/dist/style.css'; @@ -36,7 +36,7 @@ export const Workflow = ({ return ( <> - + {workflowDiagram === undefined ? null : ( diff --git a/packages/twenty-front/src/modules/workflow/components/WorkflowDiagramBaseStepNode.tsx b/packages/twenty-front/src/modules/workflow/components/WorkflowDiagramBaseStepNode.tsx new file mode 100644 index 000000000000..8c05d48baa09 --- /dev/null +++ b/packages/twenty-front/src/modules/workflow/components/WorkflowDiagramBaseStepNode.tsx @@ -0,0 +1,109 @@ +import { WorkflowDiagramStepNodeData } from '@/workflow/types/WorkflowDiagram'; +import styled from '@emotion/styled'; +import { Handle, Position } from '@xyflow/react'; +import React from 'react'; +import { capitalize } from '~/utils/string/capitalize'; + +type Variant = 'placeholder'; + +const StyledStepNodeContainer = styled.div` + display: flex; + flex-direction: column; + + padding-bottom: 12px; + padding-top: 6px; +`; + +const StyledStepNodeType = styled.div` + background-color: ${({ theme }) => theme.background.tertiary}; + border-radius: ${({ theme }) => theme.border.radius.sm} + ${({ theme }) => theme.border.radius.sm} 0 0; + + color: ${({ theme }) => theme.color.gray50}; + font-size: ${({ theme }) => theme.font.size.xs}; + font-weight: ${({ theme }) => theme.font.weight.semiBold}; + + padding: ${({ theme }) => theme.spacing(1)} ${({ theme }) => theme.spacing(2)}; + position: absolute; + top: 0; + transform: translateY(-100%); + + .selectable.selected &, + .selectable:focus &, + .selectable:focus-visible & { + background-color: ${({ theme }) => theme.color.blue}; + color: ${({ theme }) => theme.font.color.inverted}; + } +`; + +const StyledStepNodeInnerContainer = styled.div<{ variant?: Variant }>` + background-color: ${({ theme }) => theme.background.secondary}; + border: 1px solid ${({ theme }) => theme.border.color.medium}; + border-style: ${({ variant }) => + variant === 'placeholder' ? 'dashed' : null}; + border-radius: ${({ theme }) => theme.border.radius.md}; + display: flex; + gap: ${({ theme }) => theme.spacing(2)}; + padding: ${({ theme }) => theme.spacing(2)}; + + position: relative; + box-shadow: ${({ variant, theme }) => + variant === 'placeholder' ? 'none' : theme.boxShadow.superHeavy}; + + .selectable.selected &, + .selectable:focus &, + .selectable:focus-visible & { + background-color: ${({ theme }) => theme.color.blue10}; + border-color: ${({ theme }) => theme.color.blue}; + } +`; + +const StyledStepNodeLabel = styled.div<{ variant?: Variant }>` + align-items: center; + display: flex; + font-size: ${({ theme }) => theme.font.size.md}; + font-weight: ${({ theme }) => theme.font.weight.medium}; + column-gap: ${({ theme }) => theme.spacing(2)}; + color: ${({ variant, theme }) => + variant === 'placeholder' ? theme.font.color.extraLight : null}; +`; + +const StyledSourceHandle = styled(Handle)` + background-color: ${({ theme }) => theme.color.gray50}; +`; + +export const StyledTargetHandle = styled(Handle)` + visibility: hidden; +`; + +export const WorkflowDiagramBaseStepNode = ({ + nodeType, + label, + variant, + Icon, +}: { + nodeType: WorkflowDiagramStepNodeData['nodeType']; + label: string; + variant?: Variant; + Icon?: React.ReactNode; +}) => { + return ( + + {nodeType !== 'trigger' ? ( + + ) : null} + + + {capitalize(nodeType)} + + + {Icon} + + {label} + + + + + + ); +}; diff --git a/packages/twenty-front/src/modules/workflow/components/WorkflowDiagramCanvas.tsx b/packages/twenty-front/src/modules/workflow/components/WorkflowDiagramCanvas.tsx index 6d65014a8995..ef1123f4eed6 100644 --- a/packages/twenty-front/src/modules/workflow/components/WorkflowDiagramCanvas.tsx +++ b/packages/twenty-front/src/modules/workflow/components/WorkflowDiagramCanvas.tsx @@ -1,5 +1,6 @@ import { WorkflowDiagramCanvasEffect } from '@/workflow/components/WorkflowDiagramCanvasEffect'; import { WorkflowDiagramCreateStepNode } from '@/workflow/components/WorkflowDiagramCreateStepNode'; +import { WorkflowDiagramEmptyTrigger } from '@/workflow/components/WorkflowDiagramEmptyTrigger'; import { WorkflowDiagramStepNode } from '@/workflow/components/WorkflowDiagramStepNode'; import { workflowDiagramState } from '@/workflow/states/workflowDiagramState'; import { @@ -72,6 +73,7 @@ export const WorkflowDiagramCanvas = ({ nodeTypes={{ default: WorkflowDiagramStepNode, 'create-step': WorkflowDiagramCreateStepNode, + 'empty-trigger': WorkflowDiagramEmptyTrigger, }} fitView nodes={nodes.map((node) => ({ ...node, draggable: false }))} diff --git a/packages/twenty-front/src/modules/workflow/components/WorkflowDiagramEmptyTrigger.tsx b/packages/twenty-front/src/modules/workflow/components/WorkflowDiagramEmptyTrigger.tsx new file mode 100644 index 000000000000..a355733219ab --- /dev/null +++ b/packages/twenty-front/src/modules/workflow/components/WorkflowDiagramEmptyTrigger.tsx @@ -0,0 +1,30 @@ +import { WorkflowDiagramBaseStepNode } from '@/workflow/components/WorkflowDiagramBaseStepNode'; +import { useTheme } from '@emotion/react'; +import styled from '@emotion/styled'; +import { IconPlaylistAdd } from 'twenty-ui'; + +const StyledStepNodeLabelIconContainer = styled.div` + align-items: center; + background: ${({ theme }) => theme.background.transparent.light}; + border-radius: ${({ theme }) => theme.spacing(1)}; + display: flex; + justify-content: center; + padding: ${({ theme }) => theme.spacing(1)}; +`; + +export const WorkflowDiagramEmptyTrigger = () => { + const theme = useTheme(); + + return ( + + + + } + /> + ); +}; diff --git a/packages/twenty-front/src/modules/workflow/components/WorkflowDiagramStepNode.tsx b/packages/twenty-front/src/modules/workflow/components/WorkflowDiagramStepNode.tsx index fe005cc595b4..0c0e6ce945ea 100644 --- a/packages/twenty-front/src/modules/workflow/components/WorkflowDiagramStepNode.tsx +++ b/packages/twenty-front/src/modules/workflow/components/WorkflowDiagramStepNode.tsx @@ -1,67 +1,16 @@ +import { WorkflowDiagramBaseStepNode } from '@/workflow/components/WorkflowDiagramBaseStepNode'; import { WorkflowDiagramStepNodeData } from '@/workflow/types/WorkflowDiagram'; +import { useTheme } from '@emotion/react'; import styled from '@emotion/styled'; -import { Handle, Position } from '@xyflow/react'; +import { IconCode, IconPlaylistAdd } from 'twenty-ui'; -const StyledStepNodeContainer = styled.div` +const StyledStepNodeLabelIconContainer = styled.div` + align-items: center; + background: ${({ theme }) => theme.background.transparent.light}; + border-radius: ${({ theme }) => theme.spacing(1)}; display: flex; - flex-direction: column; - - padding-bottom: 12px; - padding-top: 6px; -`; - -const StyledStepNodeType = styled.div` - background-color: ${({ theme }) => theme.background.tertiary}; - border-radius: ${({ theme }) => theme.border.radius.sm} - ${({ theme }) => theme.border.radius.sm} 0 0; - - color: ${({ theme }) => theme.color.gray50}; - font-size: ${({ theme }) => theme.font.size.xs}; - font-weight: ${({ theme }) => theme.font.weight.semiBold}; - - padding: ${({ theme }) => theme.spacing(1)} ${({ theme }) => theme.spacing(2)}; - position: absolute; - top: 0; - transform: translateY(-100%); - - .selectable.selected &, - .selectable:focus &, - .selectable:focus-visible & { - background-color: ${({ theme }) => theme.color.blue}; - color: ${({ theme }) => theme.font.color.inverted}; - } -`; - -const StyledStepNodeInnerContainer = styled.div` - background-color: ${({ theme }) => theme.background.secondary}; - border: 1px solid ${({ theme }) => theme.border.color.medium}; - border-radius: ${({ theme }) => theme.border.radius.md}; - display: flex; - gap: ${({ theme }) => theme.spacing(2)}; - padding: ${({ theme }) => theme.spacing(2)}; - - position: relative; - box-shadow: ${({ theme }) => theme.boxShadow.superHeavy}; - - .selectable.selected &, - .selectable:focus &, - .selectable:focus-visible & { - background-color: ${({ theme }) => theme.color.blue10}; - border-color: ${({ theme }) => theme.color.blue}; - } -`; - -const StyledStepNodeLabel = styled.div` - font-size: ${({ theme }) => theme.font.size.md}; - font-weight: ${({ theme }) => theme.font.weight.medium}; -`; - -const StyledSourceHandle = styled(Handle)` - background-color: ${({ theme }) => theme.color.gray50}; -`; - -export const StyledTargetHandle = styled(Handle)` - visibility: hidden; + justify-content: center; + padding: ${({ theme }) => theme.spacing(1)}; `; export const WorkflowDiagramStepNode = ({ @@ -69,19 +18,37 @@ export const WorkflowDiagramStepNode = ({ }: { data: WorkflowDiagramStepNodeData; }) => { - return ( - - {data.nodeType !== 'trigger' ? ( - - ) : null} - - - {data.nodeType} + const theme = useTheme(); + + const renderStepIcon = () => { + switch (data.nodeType) { + case 'trigger': { + return ( + + + + ); + } + case 'action': { + return ( + + + + ); + } + } + + return null; + }; - {data.label} - - - - + return ( + ); }; diff --git a/packages/twenty-front/src/modules/workflow/components/WorkflowEditActionForm.tsx b/packages/twenty-front/src/modules/workflow/components/WorkflowEditActionForm.tsx index 3cddde9f02db..015952309d11 100644 --- a/packages/twenty-front/src/modules/workflow/components/WorkflowEditActionForm.tsx +++ b/packages/twenty-front/src/modules/workflow/components/WorkflowEditActionForm.tsx @@ -45,10 +45,10 @@ const StyledTriggerSettings = styled.div` export const WorkflowEditActionForm = ({ action, - onUpdateAction, + onActionUpdate, }: { action: WorkflowAction; - onUpdateAction: (trigger: WorkflowAction) => void; + onActionUpdate: (trigger: WorkflowAction) => void; }) => { const theme = useTheme(); @@ -88,7 +88,7 @@ export const WorkflowEditActionForm = ({ value={action.settings.serverlessFunctionId} options={availableFunctions} onChange={(updatedFunction) => { - onUpdateAction({ + onActionUpdate({ ...action, settings: { ...action.settings, diff --git a/packages/twenty-front/src/modules/workflow/components/WorkflowEditTriggerForm.tsx b/packages/twenty-front/src/modules/workflow/components/WorkflowEditTriggerForm.tsx index e345d3e65c42..9a3960428162 100644 --- a/packages/twenty-front/src/modules/workflow/components/WorkflowEditTriggerForm.tsx +++ b/packages/twenty-front/src/modules/workflow/components/WorkflowEditTriggerForm.tsx @@ -47,39 +47,35 @@ const StyledTriggerSettings = styled.div` export const WorkflowEditTriggerForm = ({ trigger, - onUpdateTrigger, + onTriggerUpdate, }: { - trigger: WorkflowTrigger; - onUpdateTrigger: (trigger: WorkflowTrigger) => void; + trigger: WorkflowTrigger | undefined; + onTriggerUpdate: (trigger: WorkflowTrigger) => void; }) => { const theme = useTheme(); const { activeObjectMetadataItems } = useFilteredObjectMetadataItems(); - const triggerEvent = splitWorkflowTriggerEventName( - trigger.settings.eventName, - ); + const triggerEvent = isDefined(trigger) + ? splitWorkflowTriggerEventName(trigger.settings.eventName) + : undefined; const availableMetadata: Array> = activeObjectMetadataItems.map((item) => ({ label: item.labelPlural, value: item.nameSingular, })); - const recordTypeMetadata = activeObjectMetadataItems.find( - (item) => item.nameSingular === triggerEvent.objectType, - ); - if (!isDefined(recordTypeMetadata)) { - throw new Error( - 'Expected to find the metadata configuration for the currently selected record type of the trigger.', - ); - } + const recordTypeMetadata = isDefined(triggerEvent) + ? activeObjectMetadataItems.find( + (item) => item.nameSingular === triggerEvent.objectType, + ) + : undefined; - const selectedEvent = OBJECT_EVENT_TRIGGERS.find( - (availableEvent) => availableEvent.value === triggerEvent.event, - ); - if (!isDefined(selectedEvent)) { - throw new Error('Expected to find the currently selected event type.'); - } + const selectedEvent = isDefined(triggerEvent) + ? OBJECT_EVENT_TRIGGERS.find( + (availableEvent) => availableEvent.value === triggerEvent.event, + ) + : undefined; return ( <> @@ -89,11 +85,15 @@ export const WorkflowEditTriggerForm = ({ - When a {recordTypeMetadata.labelSingular} is {selectedEvent.label} + {isDefined(recordTypeMetadata) && isDefined(selectedEvent) + ? `When a ${recordTypeMetadata.labelSingular} is ${selectedEvent.label}` + : '-'} - Trigger . Record is {selectedEvent.label} + {isDefined(selectedEvent) + ? `Trigger . Record is ${selectedEvent.label}` + : '-'} @@ -102,32 +102,50 @@ export const WorkflowEditTriggerForm = ({ dropdownId="workflow-edit-trigger-record-type" label="Record Type" fullWidth - value={triggerEvent.objectType} + value={triggerEvent?.objectType} options={availableMetadata} onChange={(updatedRecordType) => { - onUpdateTrigger({ - ...trigger, - settings: { - ...trigger.settings, - eventName: `${updatedRecordType}.${triggerEvent.event}`, - }, - }); + onTriggerUpdate( + isDefined(trigger) && isDefined(triggerEvent) + ? { + ...trigger, + settings: { + ...trigger.settings, + eventName: `${updatedRecordType}.${triggerEvent.event}`, + }, + } + : { + type: 'DATABASE_EVENT', + settings: { + eventName: `${updatedRecordType}.${OBJECT_EVENT_TRIGGERS[0].value}`, + }, + }, + ); }} />