A flow's inputs are the messages it takes from outside — a dashboard control, a run, the API — and its outputs are what a batch run reports. Both existed in the document and in the engine, and neither had any UI: the values looked hard-coded on the canvas and the Run button always used the declared defaults. The canvas now draws each as a labelled endpoint, the way it already draws a dashboard tile or another flow, skipping an input something else already accounts for. The flow panel edits them — mode, name, type, starting value, and for a live flow the value it currently holds with a way to put a new one in. Pressing Run on a batch flow asks for its parameters first. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NUb8YpL2s3gmN9WTACTt4q
76 lines
2.5 KiB
TypeScript
76 lines
2.5 KiB
TypeScript
import { Handle, type NodeProps, Position } from "@xyflow/react"
|
|
import { LayoutDashboard, LogIn, LogOut, Workflow } from "lucide-react"
|
|
import { memo } from "react"
|
|
|
|
import { useIsMobile } from "@/hooks/useMobile"
|
|
import { cn } from "@/lib/utils"
|
|
import type { EndpointNodeData } from "./endpoints"
|
|
|
|
const KIND_ICONS = {
|
|
dashboard: LayoutDashboard,
|
|
flow: Workflow,
|
|
input: LogIn,
|
|
output: LogOut,
|
|
} as const
|
|
|
|
/**
|
|
* A thing wired into this flow that is not a node in it.
|
|
*
|
|
* Drawn as a label rather than a card on purpose: a dashboard with twenty
|
|
* tiles would otherwise bury the logic the canvas exists to show, which is
|
|
* the same reason dashboards are their own documents. It is here to account
|
|
* for a value, not to compete with the nodes for attention.
|
|
*/
|
|
function EndpointNodeComponent({ data, selected }: NodeProps) {
|
|
const { label, kind, detail, provides, requires } = data as EndpointNodeData
|
|
const Icon = KIND_ICONS[kind as keyof typeof KIND_ICONS] ?? Workflow
|
|
const messages = [...provides, ...requires]
|
|
// Follows the graph's own direction; see DESIGN-GUIDELINES.md → Responsive.
|
|
const vertical = useIsMobile()
|
|
|
|
return (
|
|
<div
|
|
className={cn(
|
|
// Padding keeps the text off the connector dot, which sits on the edge.
|
|
"flex max-w-48 cursor-pointer items-center gap-2 px-3 py-1",
|
|
// Quiet at rest so the logic reads first; legible when reached for.
|
|
"text-muted-foreground/55 transition-colors",
|
|
"hover:text-foreground",
|
|
selected && "text-foreground",
|
|
)}
|
|
title={messages.join("\n")}
|
|
>
|
|
{/* Both sides always exist so an edge can attach; only one is used. */}
|
|
{requires.map((message) => (
|
|
<Handle
|
|
key={`in-${message}`}
|
|
type="target"
|
|
id={message}
|
|
position={vertical ? Position.Top : Position.Left}
|
|
className="!border-border !bg-card"
|
|
/>
|
|
))}
|
|
{provides.map((message) => (
|
|
<Handle
|
|
key={`out-${message}`}
|
|
type="source"
|
|
id={message}
|
|
position={vertical ? Position.Bottom : Position.Right}
|
|
className="!border-border !bg-card"
|
|
/>
|
|
))}
|
|
|
|
<Icon className="size-4 shrink-0" />
|
|
<span className="grid min-w-0">
|
|
<span className="truncate text-sm leading-tight">{label}</span>
|
|
<span className="truncate text-xs tracking-wide uppercase opacity-70">
|
|
{detail}
|
|
</span>
|
|
</span>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
export const EndpointNode = memo(EndpointNodeComponent)
|
|
export default EndpointNode
|