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:
@@ -10,25 +10,18 @@ import {
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { Switch } from "@/components/ui/switch"
|
||||
import { PANEL_SECTION, SidePanel } from "./SidePanel"
|
||||
|
||||
const NAME_PATTERN = /^[a-z][a-z0-9_]*$/
|
||||
import { PANEL_SECTION, PanelTitle, SidePanel } from "./SidePanel"
|
||||
|
||||
/**
|
||||
* The flow's own settings, in the same panel its nodes use.
|
||||
*
|
||||
* The name is also the namespace of every message in the flow, which is why
|
||||
* renaming goes through the server rather than being another autosaved field.
|
||||
* The flow's own settings, in the same panel its nodes use — down to where the
|
||||
* name sits and how confirming a new one works.
|
||||
*/
|
||||
export function FlowPanel({
|
||||
open,
|
||||
definition,
|
||||
nodeCount,
|
||||
renaming,
|
||||
onChange,
|
||||
onRename,
|
||||
onDelete,
|
||||
hasDraft,
|
||||
discarding,
|
||||
@@ -41,9 +34,7 @@ export function FlowPanel({
|
||||
open: boolean
|
||||
definition: FlowDef_Input
|
||||
nodeCount: number
|
||||
renaming: boolean
|
||||
onChange: (next: FlowDef_Input) => void
|
||||
onRename: (newName: string) => void
|
||||
onDelete: () => void
|
||||
hasDraft: boolean
|
||||
discarding: boolean
|
||||
@@ -53,13 +44,9 @@ export function FlowPanel({
|
||||
onToggleEnabled: (next: boolean) => void
|
||||
onClose: () => void
|
||||
}) {
|
||||
const [name, setName] = useState(definition.name)
|
||||
const [confirmOpen, setConfirmOpen] = useState(false)
|
||||
const [discardOpen, setDiscardOpen] = useState(false)
|
||||
|
||||
const valid = NAME_PATTERN.test(name)
|
||||
const changed = name !== definition.name
|
||||
|
||||
return (
|
||||
<>
|
||||
<SidePanel
|
||||
@@ -69,14 +56,11 @@ export function FlowPanel({
|
||||
bodyKey={definition.name}
|
||||
onClose={onClose}
|
||||
header={
|
||||
<Input
|
||||
<PanelTitle
|
||||
value={definition.title ?? ""}
|
||||
placeholder={definition.name}
|
||||
aria-label="Flow title"
|
||||
className="h-8 flex-1 text-sm font-medium"
|
||||
onChange={(event) =>
|
||||
onChange({ ...definition, title: event.target.value })
|
||||
}
|
||||
label="Flow title"
|
||||
onConfirm={(title) => onChange({ ...definition, title })}
|
||||
/>
|
||||
}
|
||||
footer={
|
||||
@@ -110,40 +94,11 @@ export function FlowPanel({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-2">
|
||||
<span className={PANEL_SECTION}>Name</span>
|
||||
<div className="flex items-center gap-1.5">
|
||||
<Input
|
||||
value={name}
|
||||
aria-label="Flow name"
|
||||
autoComplete="off"
|
||||
className="h-8 flex-1 font-mono text-sm"
|
||||
onChange={(event) => setName(event.target.value)}
|
||||
onKeyDown={(event) => {
|
||||
if (event.key === "Enter" && valid && changed) {
|
||||
onRename(name)
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
size="sm"
|
||||
className="h-8"
|
||||
disabled={!valid || !changed || renaming}
|
||||
onClick={() => onRename(name)}
|
||||
>
|
||||
{renaming ? "Renaming…" : "Rename"}
|
||||
</Button>
|
||||
</div>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{valid || !name
|
||||
? "Messages in this flow are named after it, so other flows reading them follow the rename."
|
||||
: "Lowercase letters, digits and underscores, starting with a letter."}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-2">
|
||||
<span className={PANEL_SECTION}>Contents</span>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
<span className="font-mono">{definition.name}</span> namespaces
|
||||
every message in here.{" "}
|
||||
{nodeCount === 0
|
||||
? "No nodes yet."
|
||||
: `${nodeCount} node${nodeCount === 1 ? "" : "s"}.`}
|
||||
|
||||
Reference in New Issue
Block a user