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:
Melvin Strobl
2026-08-15 18:43:17 +02:00
co-authored by Claude Fable 5
parent 8c82549cf6
commit 01af7787c1
27 changed files with 365 additions and 1433 deletions
+115 -7
View File
@@ -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}