|
|
@@ -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'],
|