Explorar o código

fix: pinned-data badge survived a config edit but not the reload after it

Verifying the badge in the browser (not just reading the code) turned
up a real bug in the previous commit: editing a pinned node's config
correctly flipped its badge to "changed" - but a few seconds later,
once autosave's refetch replaced the workflow, the badge silently
vanished, whether it should have shown "changed" or "unchanged."

Root cause: "Initialize nodes and edges from workflow" (an existing,
unrelated effect further up this file) rebuilds `nodes` wholesale from
scratch every time the workflow query refetches - which normal
autosave does via queryClient.invalidateQueries. The freshly-built
nodes never carry the client-only pinnedStatus/hasPinnedData fields,
and the badge-wiring effect only re-ran when isPinKept/nodeChangeStatus/
pinnedNodeData changed - none of which change just because nodes got
replaced - so nothing put the badge back.

Fixed by also watching `nodes` in the badge-wiring effect, made safe
against the render-loop this area already caused once (see the
autosave and pinned-diff commits) by only calling setNodes when
something for a given node actually differs - the same "touched"
guard the presence/lock-sync effect above it already uses. When
nothing differs, setNodes is called with the very same array
reference, so watching `nodes` cannot feed back into itself.

Verified by editing a pinned node's config, waiting past autosave's
idle window for the refetch, and confirming the badge still reflected
the correct state afterward - both for a config that reverted to
match the pin (green, unchanged) and one that diverged from it
(orange, changed). Re-ran the double-click/drag regression check
(10 clicks, 5 drags) afterward: zero console errors.
fszontagh hai 1 mes
pai
achega
9fe9d86712
Modificáronse 1 ficheiros con 40 adicións e 14 borrados
  1. 40 14
      webui/src/pages/WorkflowEditorPage.tsx

+ 40 - 14
webui/src/pages/WorkflowEditorPage.tsx

@@ -1647,20 +1647,46 @@ function WorkflowEditorInner() {
   // viewedNodes while viewing anyway, so this would never be seen then, and
   // showing it for an un-kept pin would contradict the point of "kept" -
   // that the badge means the user asked for this data to stay pinned.
+  //
+  // Also depends on `nodes` itself, unlike a first version of this effect did
+  // not: reloading the workflow (e.g. the refetch after an autosave) replaces
+  // `nodes` wholesale with freshly-built, unenriched node objects that carry
+  // none of this - so without watching `nodes`, a badge already showing could
+  // silently vanish on the next autosave with nothing to put it back. Depending
+  // on `nodes` here is only safe because of the "touched" guard below, the
+  // same pattern the presence/lock effect above uses: `setNodes` is called
+  // with the *same* array reference when nothing actually differs, so this
+  // does not feed back into itself the way an earlier version (fixed
+  // separately) of the pinned-diff recompute did.
   useEffect(() => {
-    setNodes((nds) =>
-      nds.map((n) => ({
-        ...n,
-        data: {
-          ...n.data,
-          executionState: effectiveExecutionState.nodeStates[n.id],
-          onExecute: () => executeWorkflow(),
-          onExecuteTrigger: executeTrigger,
-          pinnedStatus: isPinKept ? nodeChangeStatus[n.id] : undefined,
-          hasPinnedData: isPinKept && pinnedNodeData[n.id] !== undefined,
-        },
-      }))
-    )
+    setNodes((nds) => {
+      let touched = false
+      const next = nds.map((n) => {
+        const executionState = effectiveExecutionState.nodeStates[n.id]
+        const pinnedStatus = isPinKept ? nodeChangeStatus[n.id] : undefined
+        const hasPinnedData = isPinKept && pinnedNodeData[n.id] !== undefined
+        if (
+          n.data.executionState === executionState &&
+          n.data.pinnedStatus === pinnedStatus &&
+          n.data.hasPinnedData === hasPinnedData
+        ) {
+          return n
+        }
+        touched = true
+        return {
+          ...n,
+          data: {
+            ...n.data,
+            executionState,
+            onExecute: () => executeWorkflow(),
+            onExecuteTrigger: executeTrigger,
+            pinnedStatus,
+            hasPinnedData,
+          },
+        }
+      })
+      return touched ? next : nds
+    })
 
     // Animation only for live execution (not when viewing)
     setEdges((eds) =>
@@ -1669,7 +1695,7 @@ function WorkflowEditorInner() {
         animated: executionState.status === 'running',
       }))
     )
-  }, [effectiveExecutionState.nodeStates, executionState.status, setNodes, setEdges, executeWorkflow, isPinKept, nodeChangeStatus, pinnedNodeData])
+  }, [effectiveExecutionState.nodeStates, executionState.status, setNodes, setEdges, executeWorkflow, executeTrigger, isPinKept, nodeChangeStatus, pinnedNodeData, nodes])
 
   // Update node selection visual
   useEffect(() => {