The floating bars used to make way for a side panel, which hid Publish exactly when a node had just been edited. They keep their lane now and the panel is inset out of it; the enlarged code editor stays a floating surface between the two bars rather than covering them. Shortcuts are one small registry (`lib/shortcuts.ts`): undo, redo, ⌘K, copy, paste, and ⌘S — which publishes the flow, or applies the code when the editor has focus. Copying carries a node's own source through localStorage, so a paste works in another flow too. A flow is now named where a node is, at the top of its panel and only there, and both take effect on Confirm. Flow-level edits go through the undo stack with everything else, clicking the canvas puts the flow panel away, a failing node opens the logs filtered to its own traceback, and the panel carries its test id on a phone as well. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01H7LwYgJfpkbLCTeiAf8U4A
134 lines
3.8 KiB
TypeScript
134 lines
3.8 KiB
TypeScript
import { useQuery } from "@tanstack/react-query"
|
|
import { useNavigate } from "@tanstack/react-router"
|
|
|
|
import type { FlowSummary, NodeTypeInfo } from "@/client"
|
|
import {
|
|
CommandDialog,
|
|
CommandEmpty,
|
|
CommandGroup,
|
|
CommandInput,
|
|
CommandItem,
|
|
CommandList,
|
|
} from "@/components/ui/command"
|
|
import { libraryQueryOptions } from "./queries"
|
|
|
|
/**
|
|
* ⌘K: add a node, jump to another flow, or run the current one, without
|
|
* reaching for the dock.
|
|
*/
|
|
export function CommandPalette({
|
|
open,
|
|
onOpenChange,
|
|
nodeTypes,
|
|
flows,
|
|
onAddNode,
|
|
onAddSharedNode,
|
|
onRun,
|
|
}: {
|
|
open: boolean
|
|
onOpenChange: (open: boolean) => void
|
|
nodeTypes: NodeTypeInfo[]
|
|
flows: FlowSummary[]
|
|
onAddNode: (type: string) => void
|
|
onAddSharedNode: (libName: string) => void
|
|
onRun: () => void
|
|
}) {
|
|
const navigate = useNavigate()
|
|
const { data: libraryData } = useQuery({
|
|
...libraryQueryOptions(),
|
|
enabled: open,
|
|
})
|
|
|
|
// Picking an item re-renders the canvas, which can interrupt the dialog's
|
|
// exit animation and leave its overlay swallowing clicks. Unmounting the
|
|
// dialog outright is deterministic; the palette does not need to fade out.
|
|
if (!open) return null
|
|
|
|
const library = libraryData ?? []
|
|
|
|
const close = (action: () => void) => {
|
|
onOpenChange(false)
|
|
action()
|
|
}
|
|
|
|
return (
|
|
<CommandDialog
|
|
open={open}
|
|
onOpenChange={onOpenChange}
|
|
title="Commands"
|
|
description="Add a node, switch flow, or run"
|
|
>
|
|
<CommandInput placeholder="Add a node, switch flow, run…" />
|
|
<CommandList>
|
|
<CommandEmpty>Nothing matches that.</CommandEmpty>
|
|
|
|
<CommandGroup heading="Add a node">
|
|
{nodeTypes.map((nodeType) => (
|
|
<CommandItem
|
|
key={nodeType.type}
|
|
value={`${nodeType.title} ${nodeType.description}`}
|
|
onSelect={() => close(() => onAddNode(nodeType.type))}
|
|
>
|
|
<span className="flex flex-col">
|
|
<span>{nodeType.title}</span>
|
|
<span className="text-xs text-muted-foreground">
|
|
{nodeType.description}
|
|
</span>
|
|
</span>
|
|
</CommandItem>
|
|
))}
|
|
</CommandGroup>
|
|
|
|
{library.length > 0 ? (
|
|
<CommandGroup heading="Reusable nodes">
|
|
{library.map((entry) => (
|
|
<CommandItem
|
|
key={entry.name}
|
|
value={`shared ${entry.name}`}
|
|
onSelect={() => close(() => onAddSharedNode(entry.name))}
|
|
>
|
|
<span className="flex flex-col">
|
|
<span className="font-mono">{entry.name}</span>
|
|
<span className="text-xs text-muted-foreground">
|
|
{(entry.used_by ?? []).length === 1
|
|
? "Used once"
|
|
: `Used ${(entry.used_by ?? []).length} times`}
|
|
, code shared with every flow using it
|
|
</span>
|
|
</span>
|
|
</CommandItem>
|
|
))}
|
|
</CommandGroup>
|
|
) : null}
|
|
|
|
{flows.length > 0 ? (
|
|
<CommandGroup heading="Flows">
|
|
{flows.map((flow) => (
|
|
<CommandItem
|
|
key={flow.name}
|
|
value={`flow ${flow.name} ${flow.title}`}
|
|
onSelect={() =>
|
|
close(() =>
|
|
navigate({
|
|
to: "/flows/$flowName",
|
|
params: { flowName: flow.name },
|
|
}),
|
|
)
|
|
}
|
|
>
|
|
{flow.title || flow.name}
|
|
</CommandItem>
|
|
))}
|
|
</CommandGroup>
|
|
) : null}
|
|
|
|
<CommandGroup heading="Actions">
|
|
<CommandItem value="run flow" onSelect={() => close(onRun)}>
|
|
Run this flow
|
|
</CommandItem>
|
|
</CommandGroup>
|
|
</CommandList>
|
|
</CommandDialog>
|
|
)
|
|
}
|