Răsfoiți Sursa

fix: preserve legacy array values as strings, and a clipboard fallback for the form address

ArrayFieldEditor's array-type control converted a non-array current value
straight to [], so a nested list saved by the old string-writing bug -
Options on a form field, Apply To on a configurator entry - emptied itself
the moment someone opened it. It now parses a string value the same way
fresh input is parsed, so what was there before still shows as entries
and round-trips into a proper array on the next real edit.

The form address's Copy button called navigator.clipboard.writeText
unconditionally, which throws when the API is unavailable - plain HTTP
on a LAN, not just localhost. It now checks for the API, copies and
shows Copied on success, and falls back to selecting the address text
and saying so when the API is missing or the write is rejected.
fszontagh 1 lună în urmă
părinte
comite
d3bf7ae8a1

+ 10 - 1
webui/src/components/workflow/ArrayFieldEditor.tsx

@@ -106,8 +106,17 @@ export function ArrayFieldEditor({ fieldKey, prop, value, schema, onChange }: Ar
     // through to the plain text box below, which wrote the comma-joined
     // string itself into a setting the node reads as an array: a Select
     // field with one giant option instead of several.
+    //
+    // A value already saved by that older bug is a string, not an array -
+    // parsed the same way fresh input is, so what was typed before this fix
+    // still shows up as entries instead of silently emptying the box the
+    // moment someone opens it.
     if (p?.type === 'array') {
-      const currentArr: string[] = Array.isArray(current) ? current : []
+      const currentArr: string[] = Array.isArray(current)
+        ? current
+        : typeof current === 'string' && current !== ''
+          ? current.split(',').map((s) => s.trim()).filter((s) => s !== '')
+          : []
       return (
         <input
           type="text"

+ 56 - 4
webui/src/components/workflow/NodeConfigModal.tsx

@@ -1,5 +1,5 @@
 import { Node } from 'reactflow'
-import { useEffect, useState } from 'react'
+import { useEffect, useRef, useState } from 'react'
 import { X, Database, Maximize2, Minimize2, ExternalLink, Download, Loader2 } from 'lucide-react'
 import { useQuery } from '@tanstack/react-query'
 import Editor from '@monaco-editor/react'
@@ -106,6 +106,44 @@ export function NodeConfigModal({
   // the port to 8090 by hand.
   const formUrl = `${window.location.origin}/webhook/${workflowId}${editingConfig.path || ''}`
 
+  // navigator.clipboard is only available in a secure context - localhost
+  // counts, plain HTTP over a LAN by hostname or IP does not - so the button
+  // has to work either way and say which happened rather than fail quietly.
+  const [formUrlCopyState, setFormUrlCopyState] = useState<'idle' | 'copied' | 'manual'>('idle')
+  const formUrlRef = useRef<HTMLElement>(null)
+
+  const selectFormUrlText = () => {
+    const el = formUrlRef.current
+    const selection = window.getSelection()
+    if (!el || !selection) return
+    const range = document.createRange()
+    range.selectNodeContents(el)
+    selection.removeAllRanges()
+    selection.addRange(range)
+  }
+
+  const copyFormUrl = () => {
+    if (!navigator.clipboard?.writeText) {
+      selectFormUrlText()
+      setFormUrlCopyState('manual')
+      setTimeout(() => setFormUrlCopyState('idle'), 3000)
+      return
+    }
+    navigator.clipboard.writeText(formUrl).then(
+      () => {
+        setFormUrlCopyState('copied')
+        setTimeout(() => setFormUrlCopyState('idle'), 2000)
+      },
+      () => {
+        // The API exists but the write was refused or interrupted - the
+        // address is still selected so it can be copied by hand.
+        selectFormUrlText()
+        setFormUrlCopyState('manual')
+        setTimeout(() => setFormUrlCopyState('idle'), 3000)
+      }
+    )
+  }
+
   // Names for the credential types nodes registered, so a credential shows as
   // "SD.cpp Server" rather than the "basic" it is stored as.
   const credentialTypeLabels: Record<string, string> = {}
@@ -354,15 +392,29 @@ export function NodeConfigModal({
               <div className="rounded border border-gray-200 dark:border-slate-700 bg-gray-50 dark:bg-slate-900 p-3">
                 <div className="text-xs font-medium text-gray-600 dark:text-gray-400">Form address</div>
                 <div className="mt-1 flex items-center gap-2">
-                  <code className="flex-1 truncate text-sm text-gray-900 dark:text-gray-100">{formUrl}</code>
+                  <code
+                    ref={formUrlRef}
+                    className="flex-1 truncate text-sm text-gray-900 dark:text-gray-100"
+                  >
+                    {formUrl}
+                  </code>
                   <button
                     type="button"
-                    onClick={() => navigator.clipboard.writeText(formUrl)}
+                    onClick={copyFormUrl}
                     className="rounded border border-gray-300 dark:border-slate-600 px-2 py-1 text-xs text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-slate-800"
                   >
-                    Copy
+                    {formUrlCopyState === 'copied'
+                      ? 'Copied'
+                      : formUrlCopyState === 'manual'
+                        ? 'Selected'
+                        : 'Copy'}
                   </button>
                 </div>
+                {formUrlCopyState === 'manual' && (
+                  <p className="mt-1 text-xs text-amber-600 dark:text-amber-400">
+                    Could not copy automatically - the address is selected, so copy it by hand (Ctrl+C).
+                  </p>
+                )}
                 <p className="mt-2 text-xs text-gray-500 dark:text-gray-400">
                   Anyone with this address can open the form once the workflow is published and active.
                 </p>