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:
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user