feat: refactor workflow editor toolbar and validation components for improved usability

This commit is contained in:
mlogclub
2026-06-25 19:29:19 +08:00
parent 2f2e4ad8d0
commit 072c2f0105
@@ -43,7 +43,6 @@ import {
} from "lucide-react" } from "lucide-react"
import { useCallback, useEffect, useMemo, useRef, useState } from "react" import { useCallback, useEffect, useMemo, useRef, useState } from "react"
import { Badge } from "@/components/ui/badge"
import { Button } from "@/components/ui/button" import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input" import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label" import { Label } from "@/components/ui/label"
@@ -961,16 +960,15 @@ export function WorkflowEditor({
<WorkflowHelperLines lines={helperLines} /> <WorkflowHelperLines lines={helperLines} />
</ReactFlow> </ReactFlow>
<div className="absolute left-3 top-3 z-20 flex items-center gap-2"> <div className="absolute left-3 top-3 z-20 flex items-center gap-2">
<WorkflowValidationBadge errors={validation.errors} valid={validation.valid} /> <WorkflowCanvasToolbar
<WorkflowCanvasActions validationErrors={validation.errors}
validationValid={validation.valid}
onValidate={onValidate} onValidate={onValidate}
validateDisabled={validateDisabled} validateDisabled={validateDisabled}
onSaveDraft={onSaveDraft} onSaveDraft={onSaveDraft}
saveDraftDisabled={saveDraftDisabled} saveDraftDisabled={saveDraftDisabled}
onPublish={onPublish} onPublish={onPublish}
publishDisabled={publishDisabled} publishDisabled={publishDisabled}
/>
<WorkflowHistoryControls
canUndo={historyAvailability.canUndo} canUndo={historyAvailability.canUndo}
canRedo={historyAvailability.canRedo} canRedo={historyAvailability.canRedo}
onUndo={undoWorkflowEdit} onUndo={undoWorkflowEdit}
@@ -1597,69 +1595,43 @@ function WorkflowCanvasNode({ id, data, selected }: NodeProps<WorkflowFlowNode>)
) )
} }
function WorkflowValidationBadge({ function WorkflowCanvasToolbar({
errors, validationErrors,
valid, validationValid,
}: {
errors: string[]
valid: boolean
}) {
return (
<div className="flex gap-2">
{valid ? (
<Badge variant="default"></Badge>
) : (
<Popover>
<PopoverTrigger
render={
<button
type="button"
className="inline-flex rounded-full outline-none focus-visible:ring-2 focus-visible:ring-ring"
/>
}
>
<Badge variant="destructive" className="cursor-pointer">
{errors.length}
</Badge>
</PopoverTrigger>
<PopoverContent side="bottom" align="start" className="w-80">
<div className="text-sm font-medium">Validation issues</div>
<ul className="mt-2 max-h-72 space-y-1 overflow-y-auto text-xs text-destructive">
{errors.map((error) => (
<li key={error} className="rounded-md bg-destructive/10 px-2 py-1.5">
{error}
</li>
))}
</ul>
</PopoverContent>
</Popover>
)}
</div>
)
}
function WorkflowCanvasActions({
onValidate, onValidate,
validateDisabled, validateDisabled,
onSaveDraft, onSaveDraft,
saveDraftDisabled, saveDraftDisabled,
onPublish, onPublish,
publishDisabled, publishDisabled,
canUndo,
canRedo,
onUndo,
onRedo,
onRestoreDefault,
restoreDefaultDisabled,
}: { }: {
validationErrors: string[]
validationValid: boolean
onValidate?: () => void onValidate?: () => void
validateDisabled?: boolean validateDisabled?: boolean
onSaveDraft?: () => void onSaveDraft?: () => void
saveDraftDisabled?: boolean saveDraftDisabled?: boolean
onPublish?: () => void onPublish?: () => void
publishDisabled?: boolean publishDisabled?: boolean
canUndo: boolean
canRedo: boolean
onUndo: () => void
onRedo: () => void
onRestoreDefault?: () => void
restoreDefaultDisabled?: boolean
}) { }) {
if (!onValidate && !onSaveDraft && !onPublish) {
return null
}
return ( return (
<div className="flex overflow-hidden rounded-md border bg-background/95 shadow-sm"> <div className="flex overflow-hidden rounded-md border bg-background/95 shadow-sm">
<WorkflowValidationIndicator errors={validationErrors} valid={validationValid} />
{onValidate ? ( {onValidate ? (
<>
<WorkflowToolbarDivider />
<Button <Button
type="button" type="button"
variant="ghost" variant="ghost"
@@ -1671,54 +1643,57 @@ function WorkflowCanvasActions({
<CheckCircle2Icon className="size-3.5" /> <CheckCircle2Icon className="size-3.5" />
</Button> </Button>
</>
) : null} ) : null}
{onSaveDraft ? ( {onSaveDraft ? (
<>
<WorkflowToolbarDivider />
<Button <Button
type="button" type="button"
variant="ghost" variant="ghost"
size="sm" size="sm"
className="h-7 rounded-none border-l px-2 text-xs text-muted-foreground hover:text-foreground" className="h-7 rounded-none px-2 text-xs text-muted-foreground hover:text-foreground"
onClick={onSaveDraft} onClick={onSaveDraft}
disabled={saveDraftDisabled} disabled={saveDraftDisabled}
> >
<SaveIcon className="size-3.5" /> <SaveIcon className="size-3.5" />
稿 稿
</Button> </Button>
</>
) : null} ) : null}
{onPublish ? ( {onPublish ? (
<>
<WorkflowToolbarDivider />
<Button <Button
type="button" type="button"
variant="ghost" variant="ghost"
size="sm" size="sm"
className="h-7 rounded-none border-l px-2 text-xs font-medium text-foreground hover:text-foreground" className="h-7 rounded-none px-2 text-xs font-medium text-foreground hover:text-foreground"
onClick={onPublish} onClick={onPublish}
disabled={publishDisabled} disabled={publishDisabled}
> >
<SendIcon className="size-3.5" /> <SendIcon className="size-3.5" />
</Button> </Button>
</>
) : null} ) : null}
</div> {onRestoreDefault ? (
) <>
} <WorkflowToolbarDivider />
<Button
function WorkflowHistoryControls({ type="button"
canUndo, variant="ghost"
canRedo, size="sm"
onUndo, className="h-7 rounded-none px-2 text-xs text-muted-foreground hover:text-foreground"
onRedo, onClick={onRestoreDefault}
onRestoreDefault, disabled={restoreDefaultDisabled}
restoreDefaultDisabled, >
}: { <RotateCcwIcon className="size-3.5" />
canUndo: boolean
canRedo: boolean </Button>
onUndo: () => void </>
onRedo: () => void ) : null}
onRestoreDefault?: () => void <WorkflowToolbarDivider />
restoreDefaultDisabled?: boolean
}) {
return (
<div className="flex overflow-hidden rounded-md border bg-background/95 shadow-sm">
<Button <Button
type="button" type="button"
variant="ghost" variant="ghost"
@@ -1731,11 +1706,12 @@ function WorkflowHistoryControls({
> >
<Undo2Icon className="size-3.5" /> <Undo2Icon className="size-3.5" />
</Button> </Button>
<WorkflowToolbarDivider />
<Button <Button
type="button" type="button"
variant="ghost" variant="ghost"
size="icon" size="icon"
className="size-7 rounded-none border-l text-muted-foreground hover:text-foreground" className="size-7 rounded-none text-muted-foreground hover:text-foreground"
onClick={onRedo} onClick={onRedo}
disabled={!canRedo} disabled={!canRedo}
aria-label="反撤销" aria-label="反撤销"
@@ -1743,24 +1719,57 @@ function WorkflowHistoryControls({
> >
<Redo2Icon className="size-3.5" /> <Redo2Icon className="size-3.5" />
</Button> </Button>
{onRestoreDefault ? (
<Button
type="button"
variant="ghost"
size="icon"
className="size-7 rounded-none border-l text-muted-foreground hover:text-foreground"
onClick={onRestoreDefault}
disabled={restoreDefaultDisabled}
aria-label="恢复默认"
title="恢复默认"
>
<RotateCcwIcon className="size-3.5" />
</Button>
) : null}
</div> </div>
) )
} }
function WorkflowToolbarDivider() {
return <div className="my-1.5 h-4 w-px shrink-0 self-center bg-border/70" />
}
function WorkflowValidationIndicator({
errors,
valid,
}: {
errors: string[]
valid: boolean
}) {
if (valid) {
return (
<div className="flex h-7 items-center gap-1.5 px-2 text-xs font-medium text-primary">
<CheckCircle2Icon className="size-3.5" />
</div>
)
}
return (
<Popover>
<PopoverTrigger
render={
<button
type="button"
className="inline-flex h-7 items-center gap-1.5 px-2 text-xs font-medium text-destructive outline-none hover:bg-accent focus-visible:ring-2 focus-visible:ring-ring"
/>
}
>
<AlertCircleIcon className="size-3.5" />
{errors.length}
</PopoverTrigger>
<PopoverContent side="bottom" align="start" className="w-80">
<div className="text-sm font-medium">Validation issues</div>
<ul className="mt-2 max-h-72 space-y-1 overflow-y-auto text-xs text-destructive">
{errors.map((error) => (
<li key={error} className="rounded-md bg-destructive/10 px-2 py-1.5">
{error}
</li>
))}
</ul>
</PopoverContent>
</Popover>
)
}
function WorkflowHelperLines({ lines }: { lines: WorkflowHelperLine }) { function WorkflowHelperLines({ lines }: { lines: WorkflowHelperLine }) {
if (!lines.horizontal && !lines.vertical) { if (!lines.horizontal && !lines.vertical) {
return null return null