Răsfoiți Sursa

fix: describe a node's output from what it actually produced, not its schema

The available-data panel could only offer fields a node's schema names. A Code
node's schema can only say "result: any", so Parse Posts showed one untyped
entry with nothing inside it and no key to drag out - while the real shape,
twenty-three posts with a title, description and image URL each, sat in the
database from runs that had already happened.

Real output was used when present, but it was only ever populated by running the
workflow in that browser session. Reopening a workflow threw it away. The editor
now seeds it from the stored history when a workflow is opened.

It merges the last five runs rather than taking the newest. A run where nothing
happened - a feed with no new items - produces posts: [], which says the field
exists and nothing about what is in it. Taking only the newest would let one
quiet run erase the shape learned from every run before it, so a newer value
wins only when it describes something: an empty list or object never replaces a
populated one. Anything from this session still wins over all of it, being more
current than history.

Only completed nodes are read. A failed node's output is empty and would claim
the node produces nothing.

Verified in the browser on the reddit2image workflow: Parse Posts now expands to
count, imageCount, imagePosts and posts, and imagePosts[0] to author,
description, hasImage, id, imageDerived, imageUrl, images, isFullSize,
publishedAt, title and url, marked "23 total items" - each one draggable. RSS
Reader likewise shows the five fields it really produced rather than the eleven
its schema lists. Full suite 58/58.
fszontagh 1 lună în urmă
părinte
comite
3336e5d746
1 a modificat fișierele cu 77 adăugiri și 0 ștergeri
  1. 77 0
      webui/src/pages/WorkflowEditorPage.tsx

+ 77 - 0
webui/src/pages/WorkflowEditorPage.tsx

@@ -406,6 +406,83 @@ function WorkflowEditorInner() {
       })
   }, [searchParams, nodes.length, handleViewExecution, showToast])
 
+  // Seed the field list from the most recent run.
+  //
+  // What a node actually produced is the only reliable description of it. A
+  // Code node's schema can only say "result: any", so before this the field
+  // panel showed a single untyped entry with nothing inside it and no way to
+  // drag a key out - even though the workflow had run and the real shape was
+  // sitting in the database. Results were only ever populated by running in
+  // this browser session, so reopening a workflow lost them.
+  const seededFromExecutionRef = useRef<string | null>(null)
+  useEffect(() => {
+    if (!id || nodes.length === 0 || seededFromExecutionRef.current === id) return
+    seededFromExecutionRef.current = id
+
+    let cancelled = false
+
+    // A value that describes nothing. An empty list is the usual case: a run
+    // where the feed had no new items produces posts: [], which says the field
+    // exists but nothing about what is in it.
+    const describesNothing = (value: any) =>
+      value === undefined ||
+      value === null ||
+      (Array.isArray(value) && value.length === 0) ||
+      (typeof value === 'object' && !Array.isArray(value) && Object.keys(value).length === 0)
+
+    // Newer wins, except when the newer value describes nothing and an older
+    // one did. Otherwise a single run that found no work would erase the shape
+    // learned from every run before it.
+    const mergeOutputs = (older: any, newer: any) => {
+      if (describesNothing(newer)) return older
+      if (describesNothing(older)) return newer
+      if (
+        typeof older !== 'object' || typeof newer !== 'object' ||
+        Array.isArray(older) !== Array.isArray(newer) || Array.isArray(newer)
+      ) {
+        return newer
+      }
+      const merged: Record<string, any> = { ...older }
+      for (const [key, value] of Object.entries(newer)) {
+        merged[key] = mergeOutputs(older[key], value)
+      }
+      return merged
+    }
+
+    // A handful of runs, newest last so the merge above sees them in order.
+    executionsApi
+      .list(id, undefined, 1, 5)
+      .then(async (page: any) => {
+        const recent = (page?.executions || []).slice().reverse()
+        const seeded: Record<string, any> = {}
+        for (const summary of recent) {
+          if (cancelled) return
+          const execution: any = await executionsApi.getDetail(summary.id).catch(() => null)
+          if (!execution) continue
+          for (const nodeExecution of execution.nodeExecutions || []) {
+            const nodeId = nodeExecution.nodeId
+            // Only completed nodes describe anything. A failed one's output is
+            // empty and would claim the node produces nothing.
+            if (!nodeId || nodeExecution.status !== 'completed') continue
+            if (nodeExecution.output === undefined || nodeExecution.output === null) continue
+            seeded[nodeId] = mergeOutputs(seeded[nodeId], nodeExecution.output)
+          }
+        }
+        if (cancelled || Object.keys(seeded).length === 0) return
+        // Never over anything from this session: a node just run here is more
+        // current than the stored history.
+        setLastExecutionResults((prev) => ({ ...seeded, ...prev }))
+      })
+      .catch(() => {
+        // A workflow that has never run simply has nothing to seed. Falling
+        // back to the schema is the existing behaviour and is fine.
+      })
+
+    return () => {
+      cancelled = true
+    }
+  }, [id, nodes.length])
+
   // Fetch all available collections from the database
   const { data: allCollectionsData } = useQuery({
     queryKey: ['database-collections'],