From 72b4cf35569f80bc5e2110e5dc4aeca5e199930b Mon Sep 17 00:00:00 2001 From: stroblme Date: Mon, 12 Oct 2026 22:04:34 +0200 Subject: [PATCH] Shared Dock, PublishButton, DiscardButton, DiscardDraftDialog for the flow and dashboard editors Co-Authored-By: Claude Opus 5.5 --- frontend/src/components/Common/Dock.tsx | 196 ++++++++++++++++++ .../components/Dashboard/DashboardEditor.tsx | 147 ++++--------- frontend/src/components/Flow/FlowDock.tsx | 112 ++-------- frontend/src/components/Flow/FlowEditor.tsx | 55 ++--- 4 files changed, 277 insertions(+), 233 deletions(-) create mode 100644 frontend/src/components/Common/Dock.tsx diff --git a/frontend/src/components/Common/Dock.tsx b/frontend/src/components/Common/Dock.tsx new file mode 100644 index 0000000..ae2c505 --- /dev/null +++ b/frontend/src/components/Common/Dock.tsx @@ -0,0 +1,196 @@ +import { Check, WifiOff, X } from "lucide-react" +import { motion } from "motion/react" +import type { ReactNode } from "react" + +import { Button } from "@/components/ui/button" +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog" +import { FluksioLoader } from "@/components/ui/fluksio-loader" +import { Separator } from "@/components/ui/separator" +import { + Tooltip, + TooltipContent, + TooltipTrigger, +} from "@/components/ui/tooltip" +import { slideUp, transitions } from "@/lib/motion" +import { cn } from "@/lib/utils" + +/** + * The action bar floating at the bottom of an editor canvas, flow or dashboard. + * + * Capped and wrapping: the canvas shell clips, so an uncapped row would put the + * buttons at its ends out of reach on a phone rather than merely look wrong. + * See DESIGN-GUIDELINES.md → Responsive. + */ +export function Dock({ + className, + children, +}: { + className?: string + children: ReactNode +}) { + return ( + + {children} + + ) +} + +/** Groups the dock's buttons; a phone wraps instead. */ +export function DockSeparator() { + return ( + + ) +} + +/** + * Saved state and publish are one control: the glyph never moves, it simply + * stops being something you can press once there is nothing left to put live. + * A button that appears and disappears moved everything beside it just as the + * work was finished. + */ +export function PublishButton({ + label, + testId, + hasDraft, + saving, + publishing, + offline = false, + draftHint, + onPublish, +}: { + label: string + testId: string + hasDraft: boolean + saving: boolean + publishing: boolean + /** The engine is out of reach, so there is nothing to publish to. */ + offline?: boolean + /** What the tooltip says while there is a saved draft waiting. */ + draftHint: string + onPublish: () => void +}) { + return ( + + + + + + + + {offline + ? "Reconnecting to the engine" + : publishing + ? "Publishing" + : saving + ? "Saving" + : hasDraft + ? draftHint + : "All changes saved"} + + + ) +} + +/** Sits next to publish, and only while there is something to throw away. */ +export function DiscardButton({ + testId, + onClick, +}: { + testId: string + onClick: () => void +}) { + return ( + + + + + Discard the unpublished changes + + ) +} + +/** The confirmation behind `DiscardButton`; `children` says what goes back. */ +export function DiscardDraftDialog({ + open, + onOpenChange, + pending, + testId, + onConfirm, + children, +}: { + open: boolean + onOpenChange: (open: boolean) => void + pending: boolean + testId: string + onConfirm: () => void + children: ReactNode +}) { + return ( + + + + Discard the unpublished changes? + {children} + + + + + + + + ) +} diff --git a/frontend/src/components/Dashboard/DashboardEditor.tsx b/frontend/src/components/Dashboard/DashboardEditor.tsx index 3c722e6..fbdf360 100644 --- a/frontend/src/components/Dashboard/DashboardEditor.tsx +++ b/frontend/src/components/Dashboard/DashboardEditor.tsx @@ -1,6 +1,6 @@ import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query" import { useNavigate } from "@tanstack/react-router" -import { Check, ExternalLink, Pencil, Plus, Settings2, X } from "lucide-react" +import { Check, ExternalLink, Pencil, Plus, Settings2 } from "lucide-react" import { motion } from "motion/react" import { type CSSProperties, useEffect, useRef, useState } from "react" import { @@ -18,18 +18,15 @@ import { type Placement, type WidgetDef, } from "@/client" +import { + DiscardButton, + DiscardDraftDialog, + Dock, + DockSeparator, + PublishButton, +} from "@/components/Common/Dock" import { CanvasTitle } from "@/components/Flow/CanvasTitle" import { Button } from "@/components/ui/button" -import { - Dialog, - DialogContent, - DialogDescription, - DialogFooter, - DialogHeader, - DialogTitle, -} from "@/components/ui/dialog" -import { FluksioLoader } from "@/components/ui/fluksio-loader" -import { Separator } from "@/components/ui/separator" import { Tooltip, TooltipContent, @@ -576,16 +573,7 @@ export function DashboardEditor({ arranging never mounts the lock, so the controls are live. */} {edit ? null : } - + {edit ? ( <> {/* A canvas can be shrunk to less than what is arranged on it, @@ -669,66 +657,24 @@ export function DashboardEditor({ Dashboard settings - {/* Only while there is something to throw away. */} {hasDraft ? ( - - - - - - Discard the unpublished changes - - + setDiscardOpen(true)} + /> ) : null} - {/* Saved state and publish are one control, as in the flow dock: - the glyph stays put and simply stops being pressable. */} - - - - - - - - {publish.isPending - ? "Publishing" - : save.isPending - ? "Saving" - : hasDraft - ? "Saved — publish to put it on the panels" - : "All changes saved"} - - - - void publishDashboard()} /> + + ) : null} @@ -767,7 +713,7 @@ export function DashboardEditor({ )} {edit ? "Done" : "Edit"} - + {edit ? ( @@ -798,37 +744,20 @@ export function DashboardEditor({ onClose={() => setSettingsOpen(false)} /> - - - - Discard the unpublished changes? - - The dashboard goes back to what the panels are showing. What - you edited since is dropped. - - - - - - - - + + discard.mutate(undefined, { onSuccess: () => setEdit(false) }) + } + > + The dashboard goes back to what the panels are showing. What you + edited since is dropped. + ) : null} diff --git a/frontend/src/components/Flow/FlowDock.tsx b/frontend/src/components/Flow/FlowDock.tsx index 0d9d4fa..aff6e96 100644 --- a/frontend/src/components/Flow/FlowDock.tsx +++ b/frontend/src/components/Flow/FlowDock.tsx @@ -1,7 +1,6 @@ import { useReactFlow } from "@xyflow/react" import { AlertCircle, - Check, Maximize2, Pause, Pencil, @@ -9,16 +8,19 @@ import { PlayCircle, Plus, StepForward, - WifiOff, - X, } from "lucide-react" import { AnimatePresence, motion } from "motion/react" import { useEffect, useState } from "react" import type { ValidationIssue } from "@/client" +import { + DiscardButton, + Dock, + DockSeparator, + PublishButton, +} from "@/components/Common/Dock" import { Button } from "@/components/ui/button" import { FluksioLoader } from "@/components/ui/fluksio-loader" -import { Separator } from "@/components/ui/separator" import { Tooltip, TooltipContent, @@ -133,17 +135,7 @@ export function FlowDock({ - + - - Discard the unpublished changes - + ) : null} - {/* - * Saved state and publish are one control: the glyph never moves, it - * simply stops being something you can press once there is nothing left - * to put live. A button that appears and disappears moved everything - * beside it just as the work was finished. - */} - - - - - - - - {!connected - ? "Reconnecting to the engine" - : publishing - ? "Publishing" - : saving - ? "Saving" - : hasDraft - ? "Saved — publish to put it live" - : "All changes saved"} - - - + + ) } diff --git a/frontend/src/components/Flow/FlowEditor.tsx b/frontend/src/components/Flow/FlowEditor.tsx index 692606e..4908223 100644 --- a/frontend/src/components/Flow/FlowEditor.tsx +++ b/frontend/src/components/Flow/FlowEditor.tsx @@ -32,6 +32,7 @@ import { type MessageSpec, type NodeDef_Input, } from "@/client" +import { DiscardDraftDialog } from "@/components/Common/Dock" import { useTheme } from "@/components/theme-provider" import { Button } from "@/components/ui/button" import { @@ -1462,41 +1463,25 @@ function FlowEditorInner({ - - - - Discard the unpublished changes? - - The canvas goes back to the version the engine is running. What - you edited since is dropped, though the flow store's git history - keeps it. - - - - - - - - + + discard.mutate(undefined, { + // The published document replaces what is on the canvas, and + // the version counter goes back with it. + onSuccess: () => { + setFlowPanelOpen(false) + onReload() + }, + }) + } + > + The canvas goes back to the version the engine is running. What you + edited since is dropped, though the flow store's git history keeps it. + {/* * Not dismissable: until one version wins, every further save fails, so