Fix the MQTT node, suggest message names, and drop Items
- MQTT nodes failed to build: the topic map was renamed to talk in ports, but __slots__ still declared the old name, so every MQTT node raised AttributeError. Building one of each node type is now a test, since __slots__ makes this failure invisible until someone places the node. - Port names offer the messages already in play: everything published is worth reading, and an input nobody provides yet is worth publishing. A message only connects when both ends spell it the same way, so choosing beats typing. - Adding a port focuses its name field. - Dragging onto an input that already reads something offers the extra port as well as the replacement — an MQTT or InfluxDB node usually wants both. - The template's Item model, its routes, screens and table are gone. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016WzrvW7rjQbynnhF6pxh6i
This commit is contained in:
co-authored by
Claude Fable 5
parent
8c82549cf6
commit
01af7787c1
@@ -5,8 +5,16 @@ import { lazy, Suspense, useEffect, useRef, useState } from "react"
|
||||
|
||||
import type { DType, MessageSpec, NodeDef_Input, NodeTypeInfo } from "@/client"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import {
|
||||
Command,
|
||||
CommandEmpty,
|
||||
CommandGroup,
|
||||
CommandItem,
|
||||
CommandList,
|
||||
} from "@/components/ui/command"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { Label } from "@/components/ui/label"
|
||||
import { Popover, PopoverAnchor, PopoverContent } from "@/components/ui/popover"
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
@@ -42,19 +50,107 @@ const panelSlide = {
|
||||
},
|
||||
}
|
||||
|
||||
/**
|
||||
* A message name, typed freely or picked from the names already in play.
|
||||
*
|
||||
* The suggestions are the point: a message only connects when both ends spell
|
||||
* it the same way, so choosing beats typing.
|
||||
*/
|
||||
function MessageNameInput({
|
||||
value,
|
||||
suggestions,
|
||||
placeholder,
|
||||
autoFocus,
|
||||
onChange,
|
||||
}: {
|
||||
value: string
|
||||
suggestions: string[]
|
||||
placeholder: string
|
||||
autoFocus: boolean
|
||||
onChange: (next: string) => void
|
||||
}) {
|
||||
const [open, setOpen] = useState(false)
|
||||
const matches = suggestions.filter(
|
||||
(name) =>
|
||||
name !== value && name.toLowerCase().includes(value.toLowerCase()),
|
||||
)
|
||||
|
||||
return (
|
||||
<Popover open={open && matches.length > 0} onOpenChange={setOpen}>
|
||||
<PopoverAnchor asChild>
|
||||
<Input
|
||||
value={value}
|
||||
placeholder={placeholder}
|
||||
aria-label="Message name"
|
||||
autoComplete="off"
|
||||
// A port added by hand is meant to be named right away.
|
||||
autoFocus={autoFocus}
|
||||
className="h-8 flex-1 font-mono text-sm"
|
||||
onFocus={() => setOpen(true)}
|
||||
onBlur={() => setOpen(false)}
|
||||
onChange={(event) => {
|
||||
onChange(event.target.value)
|
||||
setOpen(true)
|
||||
}}
|
||||
onKeyDown={(event) => {
|
||||
if (event.key === "Escape") setOpen(false)
|
||||
}}
|
||||
/>
|
||||
</PopoverAnchor>
|
||||
<PopoverContent
|
||||
align="start"
|
||||
className="w-[--radix-popover-trigger-width] p-0"
|
||||
onOpenAutoFocus={(event) => event.preventDefault()}
|
||||
>
|
||||
<Command shouldFilter={false}>
|
||||
<CommandList>
|
||||
<CommandEmpty>No matching message.</CommandEmpty>
|
||||
<CommandGroup>
|
||||
{matches.map((name) => (
|
||||
<CommandItem
|
||||
key={name}
|
||||
value={name}
|
||||
className="font-mono text-sm"
|
||||
// Blur fires before click, so commit on mousedown.
|
||||
onMouseDown={(event) => {
|
||||
event.preventDefault()
|
||||
onChange(name)
|
||||
setOpen(false)
|
||||
}}
|
||||
onSelect={() => {
|
||||
onChange(name)
|
||||
setOpen(false)
|
||||
}}
|
||||
>
|
||||
{name}
|
||||
</CommandItem>
|
||||
))}
|
||||
</CommandGroup>
|
||||
</CommandList>
|
||||
</Command>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
)
|
||||
}
|
||||
|
||||
function PortList({
|
||||
title,
|
||||
specs,
|
||||
flow,
|
||||
emptyHint,
|
||||
suggestions,
|
||||
onChange,
|
||||
}: {
|
||||
title: string
|
||||
specs: MessageSpec[]
|
||||
flow: string
|
||||
emptyHint: string
|
||||
suggestions: string[]
|
||||
onChange: (next: MessageSpec[]) => void
|
||||
}) {
|
||||
// The port just added, so its name field can take focus.
|
||||
const [freshIndex, setFreshIndex] = useState<number | null>(null)
|
||||
|
||||
const update = (index: number, patch: Partial<MessageSpec>) => {
|
||||
const next = specs.map((spec, i) =>
|
||||
i === index ? { ...spec, ...patch } : spec,
|
||||
@@ -70,7 +166,10 @@ function PortList({
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-7 text-xs text-muted-foreground"
|
||||
onClick={() => onChange([...specs, { name: "", dtype: "float" }])}
|
||||
onClick={() => {
|
||||
setFreshIndex(specs.length)
|
||||
onChange([...specs, { name: "", dtype: "float" }])
|
||||
}}
|
||||
>
|
||||
Add
|
||||
</Button>
|
||||
@@ -82,14 +181,12 @@ function PortList({
|
||||
|
||||
{specs.map((spec, index) => (
|
||||
<div key={`port-${index}`} className="flex items-center gap-1.5">
|
||||
<Input
|
||||
<MessageNameInput
|
||||
value={spec.name ?? ""}
|
||||
suggestions={suggestions}
|
||||
placeholder={`name in ${flow}`}
|
||||
aria-label="Message name"
|
||||
className="h-8 flex-1 font-mono text-sm"
|
||||
onChange={(event) =>
|
||||
update(index, { name: event.target.value, port: "" })
|
||||
}
|
||||
autoFocus={index === freshIndex}
|
||||
onChange={(name) => update(index, { name, port: "" })}
|
||||
/>
|
||||
<Select
|
||||
value={spec.dtype ?? "float"}
|
||||
@@ -197,6 +294,7 @@ function PanelBody({
|
||||
node,
|
||||
flow,
|
||||
nodeType,
|
||||
suggestions,
|
||||
onChange,
|
||||
onSaveSource,
|
||||
onClose,
|
||||
@@ -205,6 +303,7 @@ function PanelBody({
|
||||
node: NodeDef_Input
|
||||
flow: string
|
||||
nodeType: NodeTypeInfo | undefined
|
||||
suggestions: PortSuggestions
|
||||
onChange: (next: NodeDef_Input) => void
|
||||
onSaveSource: (code: string) => void
|
||||
onClose: () => void
|
||||
@@ -269,6 +368,7 @@ function PanelBody({
|
||||
specs={node.requires ?? []}
|
||||
flow={flow}
|
||||
emptyHint="Nothing yet. Add a message this node reads."
|
||||
suggestions={suggestions.consumes}
|
||||
onChange={(requires) => onChange({ ...node, requires })}
|
||||
/>
|
||||
<PortList
|
||||
@@ -276,6 +376,7 @@ function PanelBody({
|
||||
specs={node.provides ?? []}
|
||||
flow={flow}
|
||||
emptyHint="Nothing yet. Add a message this node publishes."
|
||||
suggestions={suggestions.provides}
|
||||
onChange={(provides) => onChange({ ...node, provides })}
|
||||
/>
|
||||
<ParamsForm
|
||||
@@ -322,10 +423,14 @@ function PanelBody({
|
||||
* Node settings, floating over the canvas so the graph stays visible and live.
|
||||
* On a phone there is no room for that, so it becomes a full-screen sheet.
|
||||
*/
|
||||
/** Message names worth offering on each side of a node. */
|
||||
export type PortSuggestions = { consumes: string[]; provides: string[] }
|
||||
|
||||
export function NodePanel({
|
||||
node,
|
||||
flow,
|
||||
nodeTypes,
|
||||
suggestions,
|
||||
onChange,
|
||||
onSaveSource,
|
||||
onClose,
|
||||
@@ -334,6 +439,7 @@ export function NodePanel({
|
||||
node: NodeDef_Input | null
|
||||
flow: string
|
||||
nodeTypes: NodeTypeInfo[]
|
||||
suggestions: PortSuggestions
|
||||
onChange: (next: NodeDef_Input) => void
|
||||
onSaveSource: (code: string) => void
|
||||
onClose: () => void
|
||||
@@ -368,6 +474,7 @@ export function NodePanel({
|
||||
node={node}
|
||||
flow={flow}
|
||||
nodeType={nodeType}
|
||||
suggestions={suggestions}
|
||||
onChange={onChange}
|
||||
onSaveSource={onSaveSource}
|
||||
onClose={onClose}
|
||||
@@ -397,6 +504,7 @@ export function NodePanel({
|
||||
node={node}
|
||||
flow={flow}
|
||||
nodeType={nodeType}
|
||||
suggestions={suggestions}
|
||||
onChange={onChange}
|
||||
onSaveSource={onSaveSource}
|
||||
onClose={onClose}
|
||||
|
||||
Reference in New Issue
Block a user