Kaynağa Gözat

Merge branch 'seed-fields-from-history'

fszontagh 1 ay önce
ebeveyn
işleme
e2e330c98b
1 değiştirilmiş dosya ile 77 ekleme ve 0 silme
  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'],