Просмотр исходного кода

Merge branch 'declared-fields-and-help-position'

fszontagh 1 месяц назад
Родитель
Сommit
1ea77694aa

+ 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'