Browse Source

fix: narrow edge subscription, eliminate mount race, and add disabled state to Output node

- Replace broad `state.edges` subscription with count-only selector scoped to this
  node's target edges, preventing unnecessary re-renders from unrelated edge changes
- Merge two useEffect hooks into one with a null sentinel ref to prevent false
  regeneration on mount when loading saved workflows with existing connections
- Pass isExecuting={isRunning} to BaseNode so the Run button grays out during execution
- Update test mock to include edges, regenerateNode, and isRunning

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
handoff-20260429-1057
shrimbly 5 months ago
parent
commit
1075f0daf4
  1. 3
      src/components/__tests__/OutputNode.test.tsx
  2. 33
      src/components/nodes/OutputNode.tsx

3
src/components/__tests__/OutputNode.test.tsx

@ -39,6 +39,9 @@ describe("OutputNode", () => {
mockUseWorkflowStore.mockImplementation((selector) => {
const state = {
updateNodeData: mockUpdateNodeData,
regenerateNode: vi.fn(),
edges: [],
isRunning: false,
currentNodeIds: [],
groups: {},
nodes: [],

33
src/components/nodes/OutputNode.tsx

@ -15,9 +15,12 @@ export function OutputNode({ id, data, selected }: NodeProps<OutputNodeType>) {
const commentNavigation = useCommentNavigation(id);
const updateNodeData = useWorkflowStore((state) => state.updateNodeData);
const regenerateNode = useWorkflowStore((state) => state.regenerateNode);
const edges = useWorkflowStore((state) => state.edges);
const connectedEdgeCount = useWorkflowStore(
(state) => state.edges.filter((edge) => edge.target === id).length
);
const isRunning = useWorkflowStore((state) => state.isRunning);
const [showLightbox, setShowLightbox] = useState(false);
const previousEdgeCountRef = useRef<number>(0);
const previousEdgeCountRef = useRef<number | null>(null);
// Determine if content is audio
const isAudio = useMemo(() => {
@ -46,25 +49,18 @@ export function OutputNode({ id, data, selected }: NodeProps<OutputNodeType>) {
const videoBlobUrl = useVideoBlobUrl(isVideo ? contentSrc ?? null : null);
// Initialize edge count ref on mount
useEffect(() => {
const connectedEdges = edges.filter((edge) => edge.target === id);
previousEdgeCountRef.current = connectedEdges.length;
}, []); // eslint-disable-line react-hooks/exhaustive-deps
// Auto-trigger execution when connected
// Auto-trigger execution when a new connection is made
useEffect(() => {
const connectedEdges = edges.filter((edge) => edge.target === id);
const currentEdgeCount = connectedEdges.length;
// Only trigger if we gained a new connection (not on initial mount or disconnect)
if (currentEdgeCount > previousEdgeCountRef.current) {
// Auto-run when a new connection is made
if (previousEdgeCountRef.current === null) {
// First run — just record the baseline, don't trigger
previousEdgeCountRef.current = connectedEdgeCount;
return;
}
if (connectedEdgeCount > previousEdgeCountRef.current) {
regenerateNode(id);
}
previousEdgeCountRef.current = currentEdgeCount;
}, [edges, id, regenerateNode]);
previousEdgeCountRef.current = connectedEdgeCount;
}, [connectedEdgeCount, id, regenerateNode]);
// Handle Run button click
const handleRun = useCallback(() => {
@ -120,6 +116,7 @@ export function OutputNode({ id, data, selected }: NodeProps<OutputNodeType>) {
onCustomTitleChange={(title) => updateNodeData(id, { customTitle: title || undefined })}
onCommentChange={(comment) => updateNodeData(id, { comment: comment || undefined })}
onRun={handleRun}
isExecuting={isRunning}
selected={selected}
className="min-w-[200px]"
commentNavigation={commentNavigation ?? undefined}

Loading…
Cancel
Save