Find a flow's declared inputs and outputs in the global search
/search lists each input and batch output under its qualified message, and picking one opens the flow on the label's id, which the editor now treats like a click on the label: it opens the flow's settings.
This commit is contained in:
@@ -3456,7 +3456,7 @@ export const SearchEntrySchema = {
|
||||
properties: {
|
||||
category: {
|
||||
type: 'string',
|
||||
enum: ['flow', 'node', 'dashboard', 'widget', 'panel', 'secret', 'module', 'worker', 'alert'],
|
||||
enum: ['flow', 'node', 'input', 'output', 'dashboard', 'widget', 'panel', 'secret', 'module', 'worker', 'alert'],
|
||||
title: 'Category'
|
||||
},
|
||||
name: {
|
||||
|
||||
@@ -1160,14 +1160,14 @@ export type RunRequest = {
|
||||
* called, which is all the matching needs.
|
||||
*/
|
||||
export type SearchEntry = {
|
||||
category: 'flow' | 'node' | 'dashboard' | 'widget' | 'panel' | 'secret' | 'module' | 'worker' | 'alert';
|
||||
category: 'flow' | 'node' | 'input' | 'output' | 'dashboard' | 'widget' | 'panel' | 'secret' | 'module' | 'worker' | 'alert';
|
||||
name: string;
|
||||
title?: string;
|
||||
parent?: string;
|
||||
kind?: string;
|
||||
};
|
||||
|
||||
export type category = 'flow' | 'node' | 'dashboard' | 'widget' | 'panel' | 'secret' | 'module' | 'worker' | 'alert';
|
||||
export type category = 'flow' | 'node' | 'input' | 'output' | 'dashboard' | 'widget' | 'panel' | 'secret' | 'module' | 'worker' | 'alert';
|
||||
|
||||
export type SecretNames = {
|
||||
data: Array<(string)>;
|
||||
|
||||
@@ -6,6 +6,8 @@ import {
|
||||
KeyRound,
|
||||
LayoutDashboard,
|
||||
LayoutGrid,
|
||||
LogIn,
|
||||
LogOut,
|
||||
type LucideIcon,
|
||||
MonitorSmartphone,
|
||||
Package,
|
||||
@@ -45,6 +47,8 @@ const GROUPS: {
|
||||
}[] = [
|
||||
{ category: "flow", label: "Flows", icon: Workflow },
|
||||
{ category: "node", label: "Nodes", icon: Box },
|
||||
{ category: "input", label: "Inputs", icon: LogIn },
|
||||
{ category: "output", label: "Outputs", icon: LogOut },
|
||||
{ category: "dashboard", label: "Dashboards", icon: LayoutDashboard },
|
||||
{ category: "widget", label: "Widgets", icon: LayoutGrid },
|
||||
{ category: "panel", label: "Panels", icon: MonitorSmartphone },
|
||||
@@ -106,7 +110,7 @@ function couldMatch(value: string, query: string): boolean {
|
||||
* narrow as they are typed without a round trip per keystroke.
|
||||
*
|
||||
* cmdk does the matching over what is mounted, so the entries are bucketed by
|
||||
* category once per fetch rather than filtered nine times per keystroke, and
|
||||
* category once per fetch rather than filtered eleven times per keystroke, and
|
||||
* each group is capped — a list nobody can scroll to the end of is not a
|
||||
* better answer than its first twenty.
|
||||
*/
|
||||
@@ -129,7 +133,7 @@ export function GlobalSearch({
|
||||
const entries = data ?? []
|
||||
const typed = query.trim()
|
||||
const typing = typed.length > 0
|
||||
// One pass over the index rather than one per group — nine passes over
|
||||
// One pass over the index rather than one per group — eleven passes over
|
||||
// every entry on each keystroke. Not memoised: the component returns early
|
||||
// while closed, so a hook cannot go here, and one pass is already the win.
|
||||
// The same pass drops what the query cannot reach, so each bucket is already
|
||||
@@ -157,6 +161,14 @@ export function GlobalSearch({
|
||||
params: { flowName: entry.parent ?? "" },
|
||||
search: { node: entry.name },
|
||||
})
|
||||
// Arriving on the label's id does what clicking the label does.
|
||||
case "input":
|
||||
case "output":
|
||||
return navigate({
|
||||
to: "/flows/$flowName",
|
||||
params: { flowName: entry.parent ?? "" },
|
||||
search: { node: `${entry.category}:${entry.name}` },
|
||||
})
|
||||
case "dashboard":
|
||||
return navigate({
|
||||
to: "/dashboards/$name",
|
||||
|
||||
@@ -240,7 +240,7 @@ function FlowEditorInner({
|
||||
onReload,
|
||||
}: {
|
||||
flowName: string
|
||||
/** A node to arrive on, from the address bar. */
|
||||
/** A node or endpoint to arrive on, from the address bar. */
|
||||
focus?: string
|
||||
onReload: () => void
|
||||
}) {
|
||||
@@ -272,7 +272,10 @@ function FlowEditorInner({
|
||||
const [canvasNodes, setCanvasNodes, onNodesChange] = useUnpositionedNodes(
|
||||
detail.definition.nodes ?? [],
|
||||
)
|
||||
const [selectedId, setSelectedId] = useState<string | null>(focus ?? null)
|
||||
// An endpoint is never selected; arriving on one opens it instead (below).
|
||||
const [selectedId, setSelectedId] = useState<string | null>(
|
||||
focus && !isEndpointNode({ id: focus }) ? focus : null,
|
||||
)
|
||||
const [paletteOpen, setPaletteOpen] = useState(false)
|
||||
const [inspected, setInspected] = useState<InspectedEdge | null>(null)
|
||||
// The edges are derived, so xyflow's own selection would be thrown away on
|
||||
@@ -642,10 +645,18 @@ function FlowEditorInner({
|
||||
// The canvas is not remounted per node — that would throw the session away
|
||||
// on every click in the brain graph — so arriving at a flow already open
|
||||
// only changes the address. Seeded above for the first arrival, set here for
|
||||
// the ones after it.
|
||||
// the ones after it. An endpoint — an input or output found by the search —
|
||||
// does what clicking its label does, which also covers one the canvas does
|
||||
// not draw because a dashboard already accounts for it.
|
||||
useEffect(() => {
|
||||
if (focus) setSelectedId(focus)
|
||||
}, [focus])
|
||||
if (!focus) return
|
||||
if (isEndpointNode({ id: focus })) {
|
||||
openEndpoint(focus)
|
||||
} else {
|
||||
setFlowPanelOpen(false)
|
||||
setSelectedId(focus)
|
||||
}
|
||||
}, [focus, openEndpoint])
|
||||
|
||||
const selected = definitions.find((node) => node.id === selectedId) ?? null
|
||||
// A panel is the view you are working in, so it takes the room — but never
|
||||
@@ -1499,7 +1510,7 @@ export function FlowEditor({
|
||||
focus,
|
||||
}: {
|
||||
flowName: string
|
||||
/** A node to select on arrival — see `FlowEditorInner`. */
|
||||
/** A node or endpoint to arrive on — see `FlowEditorInner`. */
|
||||
focus?: string
|
||||
}) {
|
||||
const navigate = useNavigate()
|
||||
|
||||
@@ -8,9 +8,9 @@ type Search = { node?: string }
|
||||
|
||||
export const Route = createFileRoute("/_canvas/flows/$flowName")({
|
||||
component: FlowRoute,
|
||||
// Which node to arrive on. A search param rather than a hash so the brain
|
||||
// graph's click-through — and any link out of an alert — is a whole address
|
||||
// someone can send.
|
||||
// Which node, or input or output label, to arrive on. A search param rather
|
||||
// than a hash so the brain graph's click-through — and any link out of an
|
||||
// alert — is a whole address someone can send.
|
||||
validateSearch: (search: Record<string, unknown>): Search => ({
|
||||
node: typeof search.node === "string" ? search.node : undefined,
|
||||
}),
|
||||
|
||||
Reference in New Issue
Block a user