refactor: replace metadata dialog with inline editing for workflow name and description

This commit is contained in:
mlogclub
2026-07-27 21:09:01 +08:00
parent b2d28fdcf9
commit 241f274927
2 changed files with 167 additions and 58 deletions
@@ -1,20 +1,13 @@
"use client" "use client"
import { useCallback, useEffect, useState } from "react" import { useCallback, useEffect, useRef, useState } from "react"
import { PencilIcon } from "lucide-react"
import { toast } from "sonner" import { toast } from "sonner"
import { Badge } from "@/components/ui/badge" import { Badge } from "@/components/ui/badge"
import { Button } from "@/components/ui/button" import { Button } from "@/components/ui/button"
import {
Dialog,
DialogContent,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog"
import { Input } from "@/components/ui/input" import { Input } from "@/components/ui/input"
import { Textarea } from "@/components/ui/textarea"
import { import {
createAIWorkflow, createAIWorkflow,
fetchAIWorkflow, fetchAIWorkflow,
@@ -69,9 +62,12 @@ export function WorkflowWorkbench({
const [saving, setSaving] = useState(false) const [saving, setSaving] = useState(false)
const [dirty, setDirty] = useState(false) const [dirty, setDirty] = useState(false)
const [loaded, setLoaded] = useState(false) const [loaded, setLoaded] = useState(false)
const [metadataOpen, setMetadataOpen] = useState(false) const [editingField, setEditingField] = useState<
const [metadataName, setMetadataName] = useState("") "name" | "description" | null
const [metadataDescription, setMetadataDescription] = useState("") >(null)
const editSnapshotRef = useRef({ value: "", dirty: false })
const cancellingEditRef = useRef(false)
const savingRef = useRef(false)
const load = useCallback(async () => { const load = useCallback(async () => {
setLoaded(false) setLoaded(false)
@@ -104,26 +100,99 @@ export function WorkflowWorkbench({
) )
}, [load]) }, [load])
function openMetadata() { function beginEditing(field: "name" | "description") {
setMetadataName(name) cancellingEditRef.current = false
setMetadataDescription(description) editSnapshotRef.current = {
setMetadataOpen(true) value: field === "name" ? name : description,
dirty,
}
setEditingField(field)
} }
function applyMetadata() { async function finishEditing(field: "name" | "description") {
setName(metadataName) if (cancellingEditRef.current) {
setDescription(metadataDescription) cancellingEditRef.current = false
setDirty(true) return
setMetadataOpen(false) }
setEditingField(null)
const currentValue = field === "name" ? name.trim() : description.trim()
if (currentValue === editSnapshotRef.current.value) {
setDirty(editSnapshotRef.current.dirty)
return
}
if (field === "name" && !currentValue) {
setName(editSnapshotRef.current.value)
setDirty(editSnapshotRef.current.dirty)
toast.error("请填写工作流名称")
return
}
const nextName = field === "name" ? currentValue : name.trim()
const nextDescription =
field === "description" ? currentValue : description.trim()
if (savingRef.current) return
savingRef.current = true
setSaving(true)
try {
if (active) {
await updateAIWorkflow({
id: active.id,
name: nextName,
description: nextDescription,
definition: active.draftDefinition,
})
setActive((current) =>
current
? {
...current,
name: nextName,
description: nextDescription,
updatedAt: new Date().toISOString(),
}
: current
)
} else {
const created = await createAIWorkflow({
name: nextName,
description: nextDescription,
definition,
})
setActive(created)
}
setName(nextName)
setDescription(nextDescription)
setDirty(active ? editSnapshotRef.current.dirty : false)
onSaved?.()
toast.success(field === "name" ? "名称已保存" : "描述已保存")
} catch (error) {
setDirty(true)
toast.error(error instanceof Error ? error.message : "保存失败")
} finally {
savingRef.current = false
setSaving(false)
}
}
function cancelEditing(field: "name" | "description") {
cancellingEditRef.current = true
if (field === "name") {
setName(editSnapshotRef.current.value)
} else {
setDescription(editSnapshotRef.current.value)
}
setDirty(editSnapshotRef.current.dirty)
setEditingField(null)
} }
async function save() { async function save() {
if (savingRef.current) return
if (!name.trim()) { if (!name.trim()) {
toast.error("请填写工作流名称") toast.error("请填写工作流名称")
return return
} }
const savedName = name.trim() const savedName = name.trim()
const savedDescription = description.trim() const savedDescription = description.trim()
savingRef.current = true
setSaving(true) setSaving(true)
try { try {
if (active) { if (active) {
@@ -163,15 +232,18 @@ export function WorkflowWorkbench({
} catch (error) { } catch (error) {
toast.error(error instanceof Error ? error.message : "保存失败") toast.error(error instanceof Error ? error.message : "保存失败")
} finally { } finally {
savingRef.current = false
setSaving(false) setSaving(false)
} }
} }
async function publish() { async function publish() {
if (savingRef.current) return
if (!active) { if (!active) {
toast.error("请先保存") toast.error("请先保存")
return return
} }
savingRef.current = true
setSaving(true) setSaving(true)
try { try {
const version = await publishAIWorkflow(active.id, definition) const version = await publishAIWorkflow(active.id, definition)
@@ -191,6 +263,7 @@ export function WorkflowWorkbench({
} catch (error) { } catch (error) {
toast.error(error instanceof Error ? error.message : "发布失败") toast.error(error instanceof Error ? error.message : "发布失败")
} finally { } finally {
savingRef.current = false
setSaving(false) setSaving(false)
} }
} }
@@ -201,9 +274,40 @@ export function WorkflowWorkbench({
<div className="flex items-center gap-4"> <div className="flex items-center gap-4">
<div className="min-w-0 flex-1"> <div className="min-w-0 flex-1">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<h1 className="truncate text-lg font-semibold"> {editingField === "name" ? (
{name || "新建工作流"} <Input
</h1> autoFocus
aria-label="工作流名称"
value={name}
onFocus={(event) => event.currentTarget.select()}
onChange={(event) => {
setName(event.target.value)
setDirty(true)
}}
onBlur={() => void finishEditing("name")}
onKeyDown={(event) => {
if (event.key === "Enter") {
event.preventDefault()
event.currentTarget.blur()
} else if (event.key === "Escape") {
event.preventDefault()
event.stopPropagation()
cancelEditing("name")
}
}}
className="h-7 min-w-48 max-w-xl flex-1 px-2 text-lg font-semibold"
/>
) : (
<button
type="button"
aria-label="编辑工作流名称"
onClick={() => beginEditing("name")}
className="group flex h-7 min-w-0 max-w-xl items-center gap-1.5 rounded-md px-1 text-left text-lg font-semibold hover:bg-muted focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
>
<span className="truncate">{name || "新建工作流"}</span>
<PencilIcon className="size-3.5 shrink-0 text-muted-foreground opacity-0 transition-opacity group-hover:opacity-100 group-focus-visible:opacity-100" />
</button>
)}
<Badge <Badge
variant={active?.publishedVersionId ? "secondary" : "outline"} variant={active?.publishedVersionId ? "secondary" : "outline"}
> >
@@ -213,14 +317,45 @@ export function WorkflowWorkbench({
<span className="text-xs text-amber-600"></span> <span className="text-xs text-amber-600"></span>
) : null} ) : null}
</div> </div>
<p className="mt-1 line-clamp-1 text-sm text-muted-foreground"> {editingField === "description" ? (
{description || "暂未填写业务说明"} <Input
</p> autoFocus
aria-label="工作流描述"
value={description}
onFocus={(event) => event.currentTarget.select()}
onChange={(event) => {
setDescription(event.target.value)
setDirty(true)
}}
onBlur={() => void finishEditing("description")}
onKeyDown={(event) => {
if (event.key === "Enter") {
event.preventDefault()
event.currentTarget.blur()
} else if (event.key === "Escape") {
event.preventDefault()
event.stopPropagation()
cancelEditing("description")
}
}}
placeholder="添加工作流描述"
className="mt-0.5 h-6 max-w-2xl px-1 text-sm font-normal"
/>
) : (
<button
type="button"
aria-label="编辑工作流描述"
onClick={() => beginEditing("description")}
className="group mt-0.5 flex h-6 max-w-2xl items-center gap-1.5 rounded-md px-1 text-left text-sm text-muted-foreground hover:bg-muted focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
>
<span className="truncate">
{description || "添加工作流描述"}
</span>
<PencilIcon className="size-3 shrink-0 opacity-0 transition-opacity group-hover:opacity-100 group-focus-visible:opacity-100" />
</button>
)}
</div> </div>
<div className="flex shrink-0 gap-2"> <div className="flex shrink-0 gap-2">
<Button variant="outline" onClick={openMetadata}>
</Button>
<Button <Button
variant="outline" variant="outline"
disabled={saving} disabled={saving}
@@ -252,32 +387,6 @@ export function WorkflowWorkbench({
</div> </div>
)} )}
</div> </div>
<Dialog open={metadataOpen} onOpenChange={setMetadataOpen}>
<DialogContent>
<DialogHeader>
<DialogTitle></DialogTitle>
</DialogHeader>
<div className="space-y-4">
<Input
value={metadataName}
onChange={(event) => setMetadataName(event.target.value)}
placeholder="工作流名称"
/>
<Textarea
value={metadataDescription}
onChange={(event) => setMetadataDescription(event.target.value)}
placeholder="适用场景、目标与业务边界"
/>
</div>
<DialogFooter>
<Button variant="outline" onClick={() => setMetadataOpen(false)}>
</Button>
<Button onClick={applyMetadata}></Button>
</DialogFooter>
</DialogContent>
</Dialog>
</div> </div>
) )
} }
+2 -2
View File
@@ -2340,9 +2340,9 @@
"aiAgents": "Agent", "aiAgents": "Agent",
"aiWorkflows": "AI流程", "aiWorkflows": "AI流程",
"workflowRuns": "流程审计", "workflowRuns": "流程审计",
"workflows": "工作流", "workflows": "Workflow",
"agentRuns": "Agent 审计", "agentRuns": "Agent 审计",
"skillDefinition": "Skills", "skillDefinition": "Skill",
"mcp": "MCP tools", "mcp": "MCP tools",
"system": "系统管理", "system": "系统管理",
"users": "用户管理", "users": "用户管理",