diff --git a/apps/api/src/services/WorkflowService.ts b/apps/api/src/services/WorkflowService.ts index 9d64c2a..5253312 100644 --- a/apps/api/src/services/WorkflowService.ts +++ b/apps/api/src/services/WorkflowService.ts @@ -194,25 +194,57 @@ export class WorkflowService { } } - const updateData: Prisma.WorkflowUpdateInput = {}; + // Use transaction to update workflow and TRIGGER step atomically + const updated = await prisma.$transaction(async tx => { + const updateData: Prisma.WorkflowUpdateInput = {}; - if (data.name !== undefined) updateData.name = data.name; - if (data.description !== undefined) updateData.description = data.description; - if (data.triggerType !== undefined) updateData.triggerType = data.triggerType; - if (data.triggerConfig !== undefined) { - updateData.triggerConfig = data.triggerConfig === null ? Prisma.JsonNull : data.triggerConfig; - } - if (data.enabled !== undefined) updateData.enabled = data.enabled; - if (data.allowReentry !== undefined) updateData.allowReentry = data.allowReentry; + if (data.name !== undefined) updateData.name = data.name; + if (data.description !== undefined) updateData.description = data.description; + if (data.triggerType !== undefined) updateData.triggerType = data.triggerType; + if (data.triggerConfig !== undefined) { + updateData.triggerConfig = data.triggerConfig === null ? Prisma.JsonNull : data.triggerConfig; + } + if (data.enabled !== undefined) updateData.enabled = data.enabled; + if (data.allowReentry !== undefined) updateData.allowReentry = data.allowReentry; - const updated = await prisma.workflow.update({ - where: {id: workflowId}, - data: updateData, - include: { - project: { - select: {name: true}, + const updatedWorkflow = await tx.workflow.update({ + where: {id: workflowId}, + data: updateData, + include: { + project: { + select: {name: true}, + }, }, - }, + }); + + // If triggerConfig changed and it's an EVENT trigger, update the TRIGGER step + if (data.triggerConfig !== undefined && updatedWorkflow.triggerType === 'EVENT') { + const newTriggerConfig = data.triggerConfig as {eventName?: string} | null; + const eventName = newTriggerConfig?.eventName; + + if (eventName) { + // Find the TRIGGER step + const triggerStep = await tx.workflowStep.findFirst({ + where: { + workflowId: workflowId, + type: 'TRIGGER', + }, + }); + + if (triggerStep) { + // Update TRIGGER step config and name to match + await tx.workflowStep.update({ + where: {id: triggerStep.id}, + data: { + name: `Trigger: ${eventName}`, + config: {eventName}, + }, + }); + } + } + } + + return updatedWorkflow; }); // Invalidate workflow cache if enabled status changed or workflow is enabled @@ -220,6 +252,11 @@ export class WorkflowService { await EventService.invalidateWorkflowCache(projectId); } + // Also invalidate cache if triggerConfig changed on an enabled workflow + if (data.triggerConfig !== undefined && updated.enabled) { + await EventService.invalidateWorkflowCache(projectId); + } + // Send notification if enabled status changed if (data.enabled !== undefined && data.enabled !== workflow.enabled) { if (data.enabled) { diff --git a/apps/web/src/components/WorkflowBuilder.tsx b/apps/web/src/components/WorkflowBuilder.tsx index 832cea3..98704b5 100644 --- a/apps/web/src/components/WorkflowBuilder.tsx +++ b/apps/web/src/components/WorkflowBuilder.tsx @@ -226,6 +226,22 @@ function CustomNode({ onMouseLeave={() => setShowActions(false)} > {/* Action buttons - shown on hover */} + {showActions && data.type === 'TRIGGER' && ( +
+ +
+ )} {showActions && data.type !== 'TRIGGER' && (
)} + {data.type === 'TRIGGER' && data.config?.eventName && ( +
+
+ + {data.config.eventName} +
+
+ )} {data.type === 'WEBHOOK' && data.config?.url && (
@@ -375,11 +399,23 @@ export function WorkflowBuilder({workflowId, steps, onUpdate}: WorkflowBuilderPr const [stepToDelete, setStepToDelete] = useState(null); // Define handlers before they are used in useMemo - const handleEditStep = useCallback((stepId: string) => { - // This will be handled by the parent component - const event = new CustomEvent('workflow-edit-step', {detail: {stepId}}); - window.dispatchEvent(event); - }, []); + const handleEditStep = useCallback( + (stepId: string) => { + // Check if this is a TRIGGER step + const step = steps.find(s => s.id === stepId); + + if (step?.type === 'TRIGGER') { + // For TRIGGER steps, open workflow settings instead + const event = new CustomEvent('workflow-open-settings'); + window.dispatchEvent(event); + } else { + // For other steps, open step editor + const event = new CustomEvent('workflow-edit-step', {detail: {stepId}}); + window.dispatchEvent(event); + } + }, + [steps], + ); const handleDeleteStepClick = useCallback((stepId: string) => { setStepToDelete(stepId); diff --git a/apps/web/src/pages/workflows/[id].tsx b/apps/web/src/pages/workflows/[id].tsx index 0ccdef5..df336c5 100644 --- a/apps/web/src/pages/workflows/[id].tsx +++ b/apps/web/src/pages/workflows/[id].tsx @@ -318,7 +318,12 @@ export default function WorkflowEditorPage() { } }; - const handleUpdateSettings = async (data: {name: string; description?: string}) => { + const handleUpdateSettings = async (data: { + name: string; + description?: string; + allowReentry?: boolean; + triggerConfig?: {eventName: string}; + }) => { try { await network.fetch('PATCH', `/workflows/${id}`, data); toast.success('Workflow updated successfully'); @@ -352,9 +357,15 @@ export default function WorkflowEditorPage() { } }; + const handleOpenSettingsEvent = () => { + setShowSettingsDialog(true); + }; + window.addEventListener('workflow-edit-step', handleEditStepEvent); + window.addEventListener('workflow-open-settings', handleOpenSettingsEvent); return () => { window.removeEventListener('workflow-edit-step', handleEditStepEvent); + window.removeEventListener('workflow-open-settings', handleOpenSettingsEvent); }; }, [workflow]); @@ -741,21 +752,49 @@ interface SettingsDialogProps { workflow: Workflow; open: boolean; onOpenChange: (open: boolean) => void; - onSave: (data: {name: string; description?: string; allowReentry?: boolean}) => Promise; + onSave: (data: { + name: string; + description?: string; + allowReentry?: boolean; + triggerConfig?: {eventName: string}; + }) => Promise; } function SettingsDialog({workflow, open, onOpenChange, onSave}: SettingsDialogProps) { + const triggerConfig = workflow.triggerConfig as {eventName?: string} | null; const [name, setName] = useState(workflow.name); const [description, setDescription] = useState(workflow.description ?? ''); const [allowReentry, setAllowReentry] = useState(workflow.allowReentry ?? false); + const [eventName, setEventName] = useState(triggerConfig?.eventName ?? ''); const [isSubmitting, setIsSubmitting] = useState(false); + // Sync state when workflow changes or dialog opens + useEffect(() => { + if (open) { + setName(workflow.name); + setDescription(workflow.description ?? ''); + setAllowReentry(workflow.allowReentry ?? false); + const config = workflow.triggerConfig as {eventName?: string} | null; + setEventName(config?.eventName ?? ''); + } + }, [open, workflow]); + + // Fetch available event names + const {data: eventNamesData} = useSWR<{eventNames: string[]}>(open ? '/events/names' : null, { + revalidateOnFocus: false, + }); + const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); setIsSubmitting(true); try { - await onSave({name, description: description || undefined, allowReentry}); + await onSave({ + name, + description: description || undefined, + allowReentry, + triggerConfig: eventName.trim() ? {eventName: eventName.trim()} : undefined, + }); } finally { setIsSubmitting(false); } @@ -784,6 +823,36 @@ function SettingsDialog({workflow, open, onOpenChange, onSave}: SettingsDialogPr />
+
+ + {eventNamesData?.eventNames && eventNamesData.eventNames.length > 0 ? ( + + ) : ( + setEventName(e.target.value)} + placeholder="e.g., contact.created, email.opened" + required + /> + )} +

+ The event that triggers this workflow to start for a contact +

+
+
diff --git a/packages/shared/src/schemas/index.ts b/packages/shared/src/schemas/index.ts index 2aa61f1..b231984 100644 --- a/packages/shared/src/schemas/index.ts +++ b/packages/shared/src/schemas/index.ts @@ -180,6 +180,7 @@ export const WorkflowSchemas = { triggerType: z.nativeEnum(WorkflowTriggerType).optional(), triggerConfig: jsonSchema.optional(), enabled: z.boolean().optional(), + allowReentry: z.boolean().optional(), }), addStep: z.object({ type: z.nativeEnum(WorkflowStepType),