瀏覽代碼

Fix toast position and add inline workflow rename

Toast notifications:
- Move toasts below header bar (top-20) to avoid overlapping buttons
- Adjust offset for execution viewer mode (top-32)

Inline workflow rename:
- Click on workflow name to edit inline
- Show pencil icon on hover to indicate editability
- Press Enter to save, Escape to cancel
- Blur also saves the new name
- Updates hasChanges flag when renamed
fszontagh 6 月之前
父節點
當前提交
6c4e997d76

+ 69 - 3
webui/src/components/workflow/EditorHeader.tsx

@@ -1,5 +1,5 @@
-import { ArrowLeft, Save, Play, Plus, LayoutGrid, History, Pin, X, Cog, Loader2, ChevronDown } from 'lucide-react'
-import { useState, useRef, useEffect } from 'react'
+import { ArrowLeft, Save, Play, Plus, LayoutGrid, History, Pin, X, Cog, Loader2, ChevronDown, Pencil } from 'lucide-react'
+import { useState, useRef, useEffect, useCallback } from 'react'
 import type { ExecutionDetail } from '../../api/workflows'
 
 interface TriggerNode {
@@ -29,6 +29,7 @@ interface EditorHeaderProps {
   onUnpinExecution: () => void
   onShowSettings: () => void
   onToggleExecutionPanel: () => void
+  onRename?: (newName: string) => void
 }
 
 export function EditorHeader({
@@ -52,9 +53,13 @@ export function EditorHeader({
   onUnpinExecution,
   onShowSettings,
   onToggleExecutionPanel,
+  onRename,
 }: EditorHeaderProps) {
   const [showTriggerDropdown, setShowTriggerDropdown] = useState(false)
+  const [isEditingName, setIsEditingName] = useState(false)
+  const [editingName, setEditingName] = useState(workflowName)
   const dropdownRef = useRef<HTMLDivElement>(null)
+  const nameInputRef = useRef<HTMLInputElement>(null)
 
   // Close dropdown when clicking outside
   useEffect(() => {
@@ -67,6 +72,46 @@ export function EditorHeader({
     return () => document.removeEventListener('mousedown', handleClickOutside)
   }, [])
 
+  // Focus input when editing starts
+  useEffect(() => {
+    if (isEditingName && nameInputRef.current) {
+      nameInputRef.current.focus()
+      nameInputRef.current.select()
+    }
+  }, [isEditingName])
+
+  // Update editing name when workflow name changes externally
+  useEffect(() => {
+    if (!isEditingName) {
+      setEditingName(workflowName)
+    }
+  }, [workflowName, isEditingName])
+
+  const handleNameClick = useCallback(() => {
+    if (onRename && !isViewingExecution) {
+      setIsEditingName(true)
+    }
+  }, [onRename, isViewingExecution])
+
+  const handleNameSubmit = useCallback(() => {
+    const trimmedName = editingName.trim()
+    if (trimmedName && trimmedName !== workflowName && onRename) {
+      onRename(trimmedName)
+    } else {
+      setEditingName(workflowName)
+    }
+    setIsEditingName(false)
+  }, [editingName, workflowName, onRename])
+
+  const handleNameKeyDown = useCallback((e: React.KeyboardEvent) => {
+    if (e.key === 'Enter') {
+      handleNameSubmit()
+    } else if (e.key === 'Escape') {
+      setEditingName(workflowName)
+      setIsEditingName(false)
+    }
+  }, [handleNameSubmit, workflowName])
+
   const hasMultipleTriggers = triggers.length > 1
 
   return (
@@ -78,7 +123,28 @@ export function EditorHeader({
         >
           <ArrowLeft className="w-5 h-5" />
         </button>
-        <h1 className="font-semibold text-gray-900 dark:text-gray-100">{workflowName}</h1>
+        {isEditingName ? (
+          <input
+            ref={nameInputRef}
+            type="text"
+            value={editingName}
+            onChange={(e) => setEditingName(e.target.value)}
+            onBlur={handleNameSubmit}
+            onKeyDown={handleNameKeyDown}
+            className="font-semibold text-gray-900 dark:text-gray-100 bg-transparent border-b-2 border-primary-500 outline-none px-1 min-w-[200px]"
+          />
+        ) : (
+          <h1
+            onClick={handleNameClick}
+            className={`font-semibold text-gray-900 dark:text-gray-100 ${onRename && !isViewingExecution ? 'cursor-pointer hover:text-primary-600 dark:hover:text-primary-400 group flex items-center gap-1' : ''}`}
+            title={onRename && !isViewingExecution ? 'Click to rename' : undefined}
+          >
+            {workflowName}
+            {onRename && !isViewingExecution && (
+              <Pencil className="w-3.5 h-3.5 opacity-0 group-hover:opacity-50 transition-opacity" />
+            )}
+          </h1>
+        )}
         {hasChanges && (
           <span className="text-xs text-amber-600 dark:text-amber-400 bg-amber-50 dark:bg-amber-900/30 px-2 py-0.5 rounded">
             Unsaved changes (Ctrl+S)

+ 1 - 1
webui/src/components/workflow/Toast.tsx

@@ -13,7 +13,7 @@ interface ToastContainerProps {
 
 export function ToastContainer({ toasts, offsetTop = false }: ToastContainerProps) {
   return (
-    <div className={`fixed ${offsetTop ? 'top-16' : 'top-4'} right-4 z-50 space-y-2`}>
+    <div className={`fixed ${offsetTop ? 'top-32' : 'top-20'} right-4 z-50 space-y-2`}>
       {toasts.map((toast) => (
         <div
           key={toast.id}

+ 15 - 3
webui/src/pages/WorkflowEditorPage.tsx

@@ -206,6 +206,7 @@ function WorkflowEditorInner() {
   // Workflow settings state
   const [showWorkflowSettings, setShowWorkflowSettings] = useState(false)
   const [workflowSettings, setWorkflowSettings] = useState<Record<string, any>>({})
+  const [workflowName, setWorkflowName] = useState<string>('')
 
   // Execution pinning state
   const [showExecutionList, setShowExecutionList] = useState(false)
@@ -371,11 +372,14 @@ function WorkflowEditorInner() {
     queryFn: () => nodesApi.list(),
   })
 
-  // Initialize workflow settings from loaded workflow
+  // Initialize workflow settings and name from loaded workflow
   useEffect(() => {
     if (workflow?.settings) {
       setWorkflowSettings(workflow.settings)
     }
+    if (workflow?.name) {
+      setWorkflowName(workflow.name)
+    }
   }, [workflow])
 
   // Fetch all available collections from the database
@@ -1087,11 +1091,18 @@ function WorkflowEditorInner() {
     }))
 
     saveMutation.mutate({
+      name: workflowName,
       nodes: workflowNodes,
       connections: workflowConnections,
       settings: workflowSettings,
     })
-  }, [nodes, edges, saveMutation, workflowSettings])
+  }, [nodes, edges, saveMutation, workflowSettings, workflowName])
+
+  // Handle workflow rename
+  const handleRename = useCallback((newName: string) => {
+    setWorkflowName(newName)
+    setHasChanges(true)
+  }, [])
 
   // Delete edge (connection)
   const deleteEdge = useCallback((edgeId: string) => {
@@ -1984,7 +1995,7 @@ function WorkflowEditorInner() {
 
       {/* Header */}
       <EditorHeader
-        workflowName={workflow?.name || 'Workflow'}
+        workflowName={workflowName || 'Workflow'}
         hasChanges={hasChanges}
         isSaving={saveMutation.isPending}
         isRunning={executionState.status === 'running'}
@@ -2004,6 +2015,7 @@ function WorkflowEditorInner() {
         onUnpinExecution={unpinExecution}
         onShowSettings={() => setShowWorkflowSettings(true)}
         onToggleExecutionPanel={() => setShowExecutionPanel(!showExecutionPanel)}
+        onRename={handleRename}
       />
 
       {/* Main content */}