feat: refactor workflow editor toolbar and validation components for improved usability
This commit is contained in:
@@ -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,112 +1595,15 @@ 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,
|
||||||
}: {
|
|
||||||
onValidate?: () => void
|
|
||||||
validateDisabled?: boolean
|
|
||||||
onSaveDraft?: () => void
|
|
||||||
saveDraftDisabled?: boolean
|
|
||||||
onPublish?: () => void
|
|
||||||
publishDisabled?: boolean
|
|
||||||
}) {
|
|
||||||
if (!onValidate && !onSaveDraft && !onPublish) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="flex overflow-hidden rounded-md border bg-background/95 shadow-sm">
|
|
||||||
{onValidate ? (
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
variant="ghost"
|
|
||||||
size="sm"
|
|
||||||
className="h-7 rounded-none px-2 text-xs text-muted-foreground hover:text-foreground"
|
|
||||||
onClick={onValidate}
|
|
||||||
disabled={validateDisabled}
|
|
||||||
>
|
|
||||||
<CheckCircle2Icon className="size-3.5" />
|
|
||||||
校验
|
|
||||||
</Button>
|
|
||||||
) : null}
|
|
||||||
{onSaveDraft ? (
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
variant="ghost"
|
|
||||||
size="sm"
|
|
||||||
className="h-7 rounded-none border-l px-2 text-xs text-muted-foreground hover:text-foreground"
|
|
||||||
onClick={onSaveDraft}
|
|
||||||
disabled={saveDraftDisabled}
|
|
||||||
>
|
|
||||||
<SaveIcon className="size-3.5" />
|
|
||||||
保存草稿
|
|
||||||
</Button>
|
|
||||||
) : null}
|
|
||||||
{onPublish ? (
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
variant="ghost"
|
|
||||||
size="sm"
|
|
||||||
className="h-7 rounded-none border-l px-2 text-xs font-medium text-foreground hover:text-foreground"
|
|
||||||
onClick={onPublish}
|
|
||||||
disabled={publishDisabled}
|
|
||||||
>
|
|
||||||
<SendIcon className="size-3.5" />
|
|
||||||
发布流程
|
|
||||||
</Button>
|
|
||||||
) : null}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function WorkflowHistoryControls({
|
|
||||||
canUndo,
|
canUndo,
|
||||||
canRedo,
|
canRedo,
|
||||||
onUndo,
|
onUndo,
|
||||||
@@ -1710,6 +1611,14 @@ function WorkflowHistoryControls({
|
|||||||
onRestoreDefault,
|
onRestoreDefault,
|
||||||
restoreDefaultDisabled,
|
restoreDefaultDisabled,
|
||||||
}: {
|
}: {
|
||||||
|
validationErrors: string[]
|
||||||
|
validationValid: boolean
|
||||||
|
onValidate?: () => void
|
||||||
|
validateDisabled?: boolean
|
||||||
|
onSaveDraft?: () => void
|
||||||
|
saveDraftDisabled?: boolean
|
||||||
|
onPublish?: () => void
|
||||||
|
publishDisabled?: boolean
|
||||||
canUndo: boolean
|
canUndo: boolean
|
||||||
canRedo: boolean
|
canRedo: boolean
|
||||||
onUndo: () => void
|
onUndo: () => void
|
||||||
@@ -1719,6 +1628,72 @@ function WorkflowHistoryControls({
|
|||||||
}) {
|
}) {
|
||||||
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 ? (
|
||||||
|
<>
|
||||||
|
<WorkflowToolbarDivider />
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
className="h-7 rounded-none px-2 text-xs text-muted-foreground hover:text-foreground"
|
||||||
|
onClick={onValidate}
|
||||||
|
disabled={validateDisabled}
|
||||||
|
>
|
||||||
|
<CheckCircle2Icon className="size-3.5" />
|
||||||
|
校验
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
) : null}
|
||||||
|
{onSaveDraft ? (
|
||||||
|
<>
|
||||||
|
<WorkflowToolbarDivider />
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
className="h-7 rounded-none px-2 text-xs text-muted-foreground hover:text-foreground"
|
||||||
|
onClick={onSaveDraft}
|
||||||
|
disabled={saveDraftDisabled}
|
||||||
|
>
|
||||||
|
<SaveIcon className="size-3.5" />
|
||||||
|
保存草稿
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
) : null}
|
||||||
|
{onPublish ? (
|
||||||
|
<>
|
||||||
|
<WorkflowToolbarDivider />
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
className="h-7 rounded-none px-2 text-xs font-medium text-foreground hover:text-foreground"
|
||||||
|
onClick={onPublish}
|
||||||
|
disabled={publishDisabled}
|
||||||
|
>
|
||||||
|
<SendIcon className="size-3.5" />
|
||||||
|
发布流程
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
) : null}
|
||||||
|
{onRestoreDefault ? (
|
||||||
|
<>
|
||||||
|
<WorkflowToolbarDivider />
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
className="h-7 rounded-none px-2 text-xs text-muted-foreground hover:text-foreground"
|
||||||
|
onClick={onRestoreDefault}
|
||||||
|
disabled={restoreDefaultDisabled}
|
||||||
|
>
|
||||||
|
<RotateCcwIcon className="size-3.5" />
|
||||||
|
恢复默认
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
) : null}
|
||||||
|
<WorkflowToolbarDivider />
|
||||||
<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
|
||||||
|
|||||||
Reference in New Issue
Block a user