Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
'use client';

import { createContext, useContext, type ReactNode } from 'react';

/**
* Context for automation workflow callbacks
*
* IMPORTANT: This context exists to prevent infinite render loops in ReactFlow.
* ReactFlow's StoreUpdater component watches for changes in node.data objects.
* If callback functions are included in node.data, they create new object
* references on each render, triggering an infinite loop of:
* render -> new data -> StoreUpdater detects change -> setNodes -> render
*
* By passing callbacks through context instead of node.data, we maintain
* stable object references and avoid the infinite loop.
*/

interface AutomationCallbacksContextType {
/** Called when a node is clicked to open the side panel */
onNodeClick: (stepSlug: string) => void;
/** Called when the add step button on a leaf node is clicked */
onAddStep: (stepSlug: string) => void;
/** Called when adding a step on an edge */
onAddStepOnEdge: (sourceId: string, targetId: string) => void;
/** Called when deleting an edge */
onDeleteEdge: (edgeId: string) => void;
}

const AutomationCallbacksContext =
createContext<AutomationCallbacksContextType | null>(null);

export function AutomationCallbacksProvider({
children,
onNodeClick,
onAddStep,
onAddStepOnEdge,
onDeleteEdge,
}: AutomationCallbacksContextType & { children: ReactNode }) {
return (
<AutomationCallbacksContext.Provider
value={{ onNodeClick, onAddStep, onAddStepOnEdge, onDeleteEdge }}
>
{children}
</AutomationCallbacksContext.Provider>
);
}

export function useAutomationCallbacks(): AutomationCallbacksContextType {
const context = useContext(AutomationCallbacksContext);
if (!context) {
throw new Error(
'useAutomationCallbacks must be used within AutomationCallbacksProvider',
);
}
return context;
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,17 +7,16 @@ import { Doc } from '@/convex/_generated/dataModel';
import { PickaxeIcon, Repeat } from 'lucide-react';
import { Badge } from '@/components/ui/badge';
import { useT } from '@/lib/i18n';
import { useAutomationCallbacks } from './automation-callbacks-context';

interface AutomationStepProps {
data: {
label: string;
description?: string;
stepType: Doc<'wfStepDefs'>['stepType'];
stepSlug: string;
onNodeClick?: (stepSlug: string) => void;
isLeafNode?: boolean;
isTerminalNode?: boolean;
onAddStep?: () => void;
hasNextSteps?: boolean;
target?: string;
incomingCount?: number;
Expand All @@ -29,6 +28,8 @@ interface AutomationStepProps {

export default function AutomationStep({ data }: AutomationStepProps) {
const { t } = useT('automations');
const { onNodeClick } = useAutomationCallbacks();

// Determine handle positions based on whether each edge (top/bottom) has bidirectional connections
// Only offset if there are connections in both directions at that specific edge
const topTargetLeft = data.hasBidirectionalTop ? '45%' : '50%';
Expand Down Expand Up @@ -85,7 +86,7 @@ export default function AutomationStep({ data }: AutomationStepProps) {
? 'border-dashed border-2 border-muted-foreground/50'
: 'border-border',
)}
onClick={() => data.onNodeClick?.(data.stepSlug)}
onClick={() => onNodeClick(data.stepSlug)}
>
<div className="py-2 px-2.5 flex gap-3">
{/* Icon on left */}
Expand Down
Loading