Copy nodes, bind the keys, and keep publish within reach
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
This commit is contained in:
@@ -29,6 +29,7 @@ import {
|
||||
} from "@/components/ui/select"
|
||||
import { Switch } from "@/components/ui/switch"
|
||||
import useCustomToast from "@/hooks/useCustomToast"
|
||||
import { inCodeEditor, useShortcuts } from "@/lib/shortcuts"
|
||||
import { cn } from "@/lib/utils"
|
||||
import { MessageSparkline } from "./MessageSparkline"
|
||||
import {
|
||||
@@ -37,7 +38,7 @@ import {
|
||||
nodeSourceQueryOptions,
|
||||
secretsQueryOptions,
|
||||
} from "./queries"
|
||||
import { PANEL_SECTION, SidePanel } from "./SidePanel"
|
||||
import { PANEL_SECTION, PanelTitle, SidePanel } from "./SidePanel"
|
||||
|
||||
const NodeEditor = lazy(() => import("./NodeEditor"))
|
||||
|
||||
@@ -679,6 +680,15 @@ function PanelBody({
|
||||
const save = useRef(onSaveSource)
|
||||
save.current = onSaveSource
|
||||
|
||||
/** Send what is typed now rather than a second from now. */
|
||||
const saveNow = () => {
|
||||
if (timer.current) {
|
||||
clearTimeout(timer.current)
|
||||
timer.current = null
|
||||
}
|
||||
if (pending.current !== null) save.current(pending.current)
|
||||
}
|
||||
|
||||
const editCode = (next: string) => {
|
||||
setCode(next)
|
||||
pending.current = next
|
||||
@@ -689,6 +699,17 @@ function PanelBody({
|
||||
}, 1000)
|
||||
}
|
||||
|
||||
// ⌘S in the editor means "apply this code"; the editor's own state is here,
|
||||
// so the binding is too. Anywhere else on the canvas it publishes the flow.
|
||||
useShortcuts(
|
||||
{
|
||||
"mod+s": (event) => {
|
||||
if (inCodeEditor(event.target)) saveNow()
|
||||
},
|
||||
},
|
||||
["mod+s"],
|
||||
)
|
||||
|
||||
// Closing the panel must not lose the last keystrokes.
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
@@ -827,13 +848,10 @@ export function NodePanel({
|
||||
<span className="min-w-0 truncate font-mono text-sm text-muted-foreground">
|
||||
/{flow}/
|
||||
</span>
|
||||
<Input
|
||||
<PanelTitle
|
||||
value={node.title || node.id}
|
||||
aria-label="Node name"
|
||||
className="h-8 flex-1 text-sm font-medium"
|
||||
onChange={(event) =>
|
||||
onChange({ ...node, title: event.target.value })
|
||||
}
|
||||
label="Node name"
|
||||
onConfirm={(title) => onChange({ ...node, title })}
|
||||
/>
|
||||
</div>
|
||||
) : null
|
||||
|
||||
Reference in New Issue
Block a user