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}`,
+ },
+ },
+ );
}}
/>