Dashboard message picker: searchable combobox instead of a plain select

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-12 22:06:31 +02:00
co-authored by Claude Opus 5.5
parent 0ef7d027b8
commit cfd2556053
+63 -22
View File
@@ -1,5 +1,5 @@
import { useQuery } from "@tanstack/react-query"
import { Ban, ChevronDown, Plus, RotateCw, X } from "lucide-react"
import { Ban, Check, ChevronDown, Plus, RotateCw, X } from "lucide-react"
import { useState } from "react"
import type { MessageInfo, SettingDef, WidgetDef } from "@/client"
@@ -14,6 +14,14 @@ import {
} from "@/components/Flow/SidePanel"
import { runOverviewQueryOptions } from "@/components/Runs/queries"
import { Button } from "@/components/ui/button"
import {
Command,
CommandEmpty,
CommandGroup,
CommandInput,
CommandItem,
CommandList,
} from "@/components/ui/command"
import {
Dialog,
DialogContent,
@@ -97,7 +105,12 @@ function choicesFor(kind: WidgetKind, catalog: MessageInfo[]): MessageInfo[] {
)
}
/** The picker, which records the payload type it bound along with the name. */
/**
* The picker, which records the payload type it bound along with the name.
*
* Searchable, as the flow editor's message fields are: the catalog is every
* message every flow declares.
*/
function MessagePicker({
kind,
value,
@@ -119,6 +132,7 @@ function MessagePicker({
filter?: (message: MessageInfo) => boolean
onPick: (message: string, dtype: string) => void
}) {
const [open, setOpen] = useState(false)
const { data } = useQuery(messageCatalogQueryOptions())
const catalog = data?.data ?? []
const choices = filter
@@ -130,26 +144,53 @@ function MessagePicker({
return (
<div className="grid gap-1.5">
<Label className="text-sm font-normal">{label}</Label>
<Select
value={value}
onValueChange={(next) =>
onPick(
next,
choices.find((message) => message.name === next)?.dtype ?? "",
)
}
>
<SelectTrigger data-testid={testId}>
<SelectValue placeholder={placeholder} />
</SelectTrigger>
<SelectContent>
{choices.map((message) => (
<SelectItem key={message.name} value={message.name}>
{message.name}
</SelectItem>
))}
</SelectContent>
</Select>
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger asChild>
<Button
variant="outline"
role="combobox"
aria-expanded={open}
aria-label={label}
// Sits in a column of fields, so it wears their border and
// surface rather than a button's.
className="justify-start border-input bg-transparent font-normal"
data-testid={testId}
>
<span className={cn("truncate", !value && "text-muted-foreground")}>
{value || placeholder}
</span>
<ChevronDown className="ml-auto opacity-50" />
</Button>
</PopoverTrigger>
<PopoverContent
align="start"
className="w-[var(--radix-popover-trigger-width)] p-0"
>
<Command>
<CommandInput placeholder="Search messages" />
<CommandList>
<CommandEmpty>No message by that name.</CommandEmpty>
<CommandGroup>
{choices.map((message) => (
<CommandItem
key={message.name}
value={message.name}
onSelect={() => {
onPick(message.name, message.dtype ?? "")
setOpen(false)
}}
>
<span className="truncate">{message.name}</span>
{message.name === value ? (
<Check className="ml-auto" />
) : null}
</CommandItem>
))}
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>
</Popover>
</div>
)
}