Преглед изворни кода

fix: offer fields a node declares in its config, and stop the syntax help covering the text

DECLARED FIELDS WERE INVISIBLE

Some nodes say what they produce in their own configuration rather than in a
schema: Workflow Input lists the values it accepts, Set lists the fields it
writes, Configurator lists the settings it supplies. The available-data panel
only knew about execution results and output schemas, so those names were
offered nowhere - even though they are known the moment they are typed, and a
Workflow Input's whole purpose is to name what later nodes will use.

A node can now declare outputsFromConfig, naming the setting that holds the
list and the key that holds each name. Four nodes do.

THE SYNTAX HELP SAT ON TOP OF THE FIELD

It was positioned at the caret. In a single-line box that is below the text; in
a textarea it is in the middle of it, so a block of reference text covered the
prompt being written and the field could not be edited while it was up. It is a
reminder, not something to pick from, so it never needed to be near the caret -
it now sits under the field in the normal flow, on one line.

A NAMELESS INPUT IS NOW AN ERROR

Found while checking the panel: a Workflow Input in a real workflow had a
description, a default and required set, but no name - the key had gone into the
Default box. The node skipped nameless entries silently, so the caller's value
was never accepted and the failure surfaced two nodes later as "txt2img needs a
prompt". A row with any other content but no name now fails where the mistake
is, quoting the description and default so the row is identifiable.

Verified in the browser: with a prompt being typed, nothing covers the textarea.
Full suite 58/58.
fszontagh пре 1 месец
родитељ
комит
c5f38497bf

+ 3 - 0
nodes/core/configurator.js

@@ -9,6 +9,9 @@
 
 const configSchema = {
     type: 'object',
+    // The names below are what this node produces, so the editor can offer
+    // them to later nodes as soon as they are typed - without waiting for a run.
+    outputsFromConfig: { from: 'settings', nameKey: 'name' },
     properties: {
         settings: {
             type: 'array',

+ 3 - 0
nodes/core/set-fields.js

@@ -9,6 +9,9 @@
 
 const configSchema = {
     type: 'object',
+    // The names below are what this node produces, so the editor can offer
+    // them to later nodes as soon as they are typed - without waiting for a run.
+    outputsFromConfig: { from: 'fields', nameKey: 'name' },
     properties: {
         mode: {
             type: 'string',

+ 3 - 0
nodes/core/workflow-output.js

@@ -9,6 +9,9 @@
 
 const configSchema = {
     type: 'object',
+    // The names below are what this node produces, so the editor can offer
+    // them to later nodes as soon as they are typed - without waiting for a run.
+    outputsFromConfig: { from: 'fields', nameKey: 'name' },
     properties: {
         source: {
             type: 'string',

+ 19 - 1
nodes/triggers/workflow-input.js

@@ -10,6 +10,9 @@
 
 const configSchema = {
     type: 'object',
+    // The names below are what this node produces, so the editor can offer
+    // them to later nodes as soon as they are typed - without waiting for a run.
+    outputsFromConfig: { from: 'fields', nameKey: 'name' },
     properties: {
         fields: {
             type: 'array',
@@ -55,7 +58,22 @@ async function execute(config, input, context) {
     for (let i = 0; i < fields.length; i++) {
         const field = fields[i] || {};
         const name = String(field.name || '').trim();
-        if (!name) continue;
+        if (!name) {
+            // A row with a description or a default but no name is a filled-in
+            // row whose key went in the wrong box. Skipping it silently meant
+            // the caller's value was never accepted and the failure surfaced
+            // somewhere else entirely - a node further on complaining that
+            // something it needed was missing.
+            const described = String(field.description || '').trim();
+            const fallback = String(field.defaultValue || '').trim();
+            if (described || fallback || field.required === true) {
+                throw new Error('Workflow Input: an expected input has no name' +
+                    (described ? ' (described as "' + described + '")' : '') +
+                    (fallback ? ', default "' + fallback + '"' : '') +
+                    '. Put the key the caller passes in the Name box');
+            }
+            continue;
+        }
 
         const has = Object.prototype.hasOwnProperty.call(given, name) &&
                     given[name] !== undefined && given[name] !== null && given[name] !== '';

+ 16 - 16
webui/src/components/ExpressionInput.tsx

@@ -444,23 +444,23 @@ export function ExpressionInput({
         </div>
       )}
 
-      {/* Expression syntax help */}
+      {/* Expression syntax help.
+          Below the field, in the normal flow - not floating at the cursor.
+          It used to be positioned where the caret was, which inside a textarea
+          put a block of reference text directly over the words being written,
+          and a prompt could not be edited at all while it was up. It is a
+          reminder, not something to pick from, so it never needs to be near
+          the caret. */}
       {showAutocomplete && filteredFields.length === 0 && (
-        <div
-          ref={autocompleteRef}
-          className="absolute z-50 w-full bg-white dark:bg-slate-800 border border-gray-200 dark:border-slate-700 rounded-lg shadow-lg mt-1 p-3"
-          style={{
-            top: autocompletePosition.top,
-            left: autocompletePosition.left,
-          }}
-        >
-          <div className="text-xs text-gray-600 dark:text-gray-400">
-            <div className="font-medium mb-2">Expression Syntax:</div>
-            <ul className="space-y-1 text-gray-500 dark:text-gray-400">
-              <li><code className="bg-gray-100 dark:bg-slate-700 px-1 rounded">data.field</code> - Access input data</li>
-              <li><code className="bg-gray-100 dark:bg-slate-700 px-1 rounded">$node["Name"].field</code> - Access node output</li>
-              <li><code className="bg-gray-100 dark:bg-slate-700 px-1 rounded">$trigger.timestamp</code> - Trigger data</li>
-            </ul>
+        <div className="mt-1 rounded-lg border border-gray-200 dark:border-slate-700 bg-gray-50 dark:bg-slate-800/60 px-3 py-2">
+          <div className="text-[11px] text-gray-600 dark:text-gray-400">
+            <span className="font-medium">Expression syntax: </span>
+            <code className="bg-gray-100 dark:bg-slate-700 px-1 rounded">data.field</code>
+            <span className="text-gray-400"> input data · </span>
+            <code className="bg-gray-100 dark:bg-slate-700 px-1 rounded">$node["Name"].field</code>
+            <span className="text-gray-400"> another node · </span>
+            <code className="bg-gray-100 dark:bg-slate-700 px-1 rounded">$trigger.timestamp</code>
+            <span className="text-gray-400"> trigger data</span>
           </div>
         </div>
       )}

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

@@ -2052,6 +2052,34 @@ function WorkflowEditorInner() {
       } else {
         const nodeDef = nodeDefsMap[sourceNode.data.type]
         if (nodeDef) {
+          // Some nodes say what they produce in their own configuration rather
+          // than in a schema: a Workflow Input lists the values it accepts, a
+          // Set node lists the fields it writes. Those names are known the
+          // moment they are typed, so they are offered straight away instead of
+          // waiting for a run that may not have happened yet.
+          const declares = (nodeDef.configSchema as any)?.outputsFromConfig
+          const declaredList = declares
+            ? (sourceNode.data.config || {})[declares.from]
+            : undefined
+
+          if (Array.isArray(declaredList) && declaredList.length > 0) {
+            const nameKey = declares.nameKey || 'name'
+            for (const entry of declaredList) {
+              const name = String(entry?.[nameKey] ?? '').trim()
+              if (!name) continue
+              fields.push({
+                path: name,
+                type: 'any',
+                nodeName,
+                nodeId: sourceNode.id,
+                displayName: name,
+                isDirectUpstream,
+                targetInput,
+              })
+            }
+            continue
+          }
+
           if (sourceNode.data.type === 'loop') {
             const loopConfig = sourceNode.data.config || {}
             const itemVariable = loopConfig.itemVariableName || 'item'