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) => { mockUseWorkflowStore.mockImplementation((selector) => {
const state = { const state = {
updateNodeData: mockUpdateNodeData, updateNodeData: mockUpdateNodeData,
regenerateNode: vi.fn(),
edges: [],
isRunning: false,
currentNodeIds: [], currentNodeIds: [],
groups: {}, groups: {},
nodes: [], nodes: [],

33
src/components/nodes/OutputNode.tsx

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

Loading…
Cancel
Save