From 5a4389487577e2d1257f8c1218bc4fd6e51db837 Mon Sep 17 00:00:00 2001 From: Developer Date: Mon, 6 Jul 2026 14:51:14 +0000 Subject: [PATCH] Fix: sheet scroll, direct-edit close, mobile copy btn, service badge MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Four fixes: 1. Mobile edit fullscreen scroll: the Sheet primitive's data-[side=bottom]:h-auto was overriding our h-[100dvh] on SheetForm, preventing scroll. Added data-[side=bottom]:h-[100dvh] to the SheetForm className to win the specificity battle. 2. Direct-edit close showed list view: when opened via editWidgetId (the hover edit button), saving or canceling called reset() which showed the widget list instead of closing the dialog. Now derives directEdit from editWidgetId — when true, reset() calls onClose() to close entirely. 3. Copy button missing on mobile: MobileWidgetSections didn't pass onCopy to its WidgetInstanceCard instances. Now accepts and wires onCopyWidget, so referenced widgets show the copy/detach button on mobile too. 4. Service enabled badge stale: ServiceConfigEditor showed instance.enabled (the initial prop) instead of the local enabled state. Now reads the local enabled variable so the badge updates when the user toggles the switch. 128 tests pass; 0 lint errors; build clean. --- .../src/components/WidgetConfigDialog.tsx | 9 + frontend/src/components/WidgetInstance.tsx | 4 +- frontend/src/components/ui/dialog.tsx | 240 +++++++++--------- frontend/src/components/ui/sheet-form.tsx | 2 +- frontend/src/pages/Dashboard.tsx | 19 +- frontend/src/pages/Settings.tsx | 4 +- 6 files changed, 148 insertions(+), 130 deletions(-) diff --git a/frontend/src/components/WidgetConfigDialog.tsx b/frontend/src/components/WidgetConfigDialog.tsx index 5638792..04d0d1c 100644 --- a/frontend/src/components/WidgetConfigDialog.tsx +++ b/frontend/src/components/WidgetConfigDialog.tsx @@ -228,6 +228,9 @@ export function WidgetConfigDialog({ const [draft, setDraft] = useState(null); const [draftBaseline, setDraftBaseline] = useState(null); + // When opened via editWidgetId, closing the edit should close the dialog + // entirely (not fall back to the list view). + const directEdit = Boolean(editWidgetId); // When editWidgetId is set and the dialog opens, auto-enter edit mode for // that widget (instead of showing the list view). @@ -281,6 +284,12 @@ export function WidgetConfigDialog({ } function reset() { + // If we were opened via direct edit, closing should close the dialog + // entirely, not fall back to the list view. + if (directEdit) { + onClose(); + return; + } setDraft(null); setDraftBaseline(null); } diff --git a/frontend/src/components/WidgetInstance.tsx b/frontend/src/components/WidgetInstance.tsx index 3d6e017..96e0552 100644 --- a/frontend/src/components/WidgetInstance.tsx +++ b/frontend/src/components/WidgetInstance.tsx @@ -52,7 +52,7 @@ export function WidgetInstanceCard({ widget, onEdit, onCopy }: Props) { : `Unknown widget: ${widget.widget_kind} (built-in)`; return (
- {(onEdit || onCopy) ? actionButtons : null} + {onEdit || onCopy ? actionButtons : null} {label} @@ -65,7 +65,7 @@ export function WidgetInstanceCard({ widget, onEdit, onCopy }: Props) { const Component = resolved.component; return (
- {(onEdit || onCopy) ? actionButtons : null} + {onEdit || onCopy ? actionButtons : null} ) { - return + return ; } function DialogTrigger({ - ...props + ...props }: React.ComponentProps) { - return + return ; } function DialogPortal({ - ...props + ...props }: React.ComponentProps) { - return + return ; } function DialogClose({ - ...props + ...props }: React.ComponentProps) { - return + return ; } function DialogOverlay({ - className, - ...props + className, + ...props }: React.ComponentProps) { - return ( - - ) + return ( + + ); } function DialogContent({ - className, - children, - showCloseButton = true, - ...props + className, + children, + showCloseButton = true, + ...props }: React.ComponentProps & { - showCloseButton?: boolean + showCloseButton?: boolean; }) { - return ( - - - - {children} - {showCloseButton && ( - - - - )} - - - ) + return ( + + + + {children} + {showCloseButton && ( + + + + )} + + + ); } function DialogHeader({ className, ...props }: React.ComponentProps<"div">) { - return ( -
- ) + return ( +
+ ); } function DialogFooter({ - className, - showCloseButton = false, - children, - ...props + className, + showCloseButton = false, + children, + ...props }: React.ComponentProps<"div"> & { - showCloseButton?: boolean + showCloseButton?: boolean; }) { - return ( -
- {children} - {showCloseButton && ( - - - - )} -
- ) + return ( +
+ {children} + {showCloseButton && ( + + + + )} +
+ ); } function DialogTitle({ - className, - ...props + className, + ...props }: React.ComponentProps) { - return ( - - ) + return ( + + ); } function DialogDescription({ - className, - ...props + className, + ...props }: React.ComponentProps) { - return ( - - ) + return ( + + ); } export { - Dialog, - DialogClose, - DialogContent, - DialogDescription, - DialogFooter, - DialogHeader, - DialogOverlay, - DialogPortal, - DialogTitle, - DialogTrigger, -} + Dialog, + DialogClose, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogOverlay, + DialogPortal, + DialogTitle, + DialogTrigger, +}; diff --git a/frontend/src/components/ui/sheet-form.tsx b/frontend/src/components/ui/sheet-form.tsx index 452158a..12e2130 100644 --- a/frontend/src/components/ui/sheet-form.tsx +++ b/frontend/src/components/ui/sheet-form.tsx @@ -82,7 +82,7 @@ export function SheetForm({ { // Prevent Radix's default Escape close so our guard runs instead. if (isDirty) { diff --git a/frontend/src/pages/Dashboard.tsx b/frontend/src/pages/Dashboard.tsx index c618d06..ff8bb70 100644 --- a/frontend/src/pages/Dashboard.tsx +++ b/frontend/src/pages/Dashboard.tsx @@ -31,7 +31,11 @@ import { useDeleteDashboardShortcut, useSaveDashboardShortcut, } from "../hooks/useDashboard"; -import { useDetachWidgetReference, useWidgetInstances, useWidgetReferences } from "../hooks/useWidgets"; +import { + useDetachWidgetReference, + useWidgetInstances, + useWidgetReferences, +} from "../hooks/useWidgets"; import { useServiceInstances } from "../hooks/useServices"; import { useIsMobile } from "../hooks/useIsMobile"; import type { @@ -98,9 +102,11 @@ function groupWidgetsBySection( function MobileWidgetSections({ sections, onEditWidget, + onCopyWidget, }: { sections: { id: SectionId; widgets: WidgetInstance[] }[]; onEditWidget?: (widgetId: string) => void; + onCopyWidget?: (widgetId: string) => void; }) { return ( <> @@ -143,6 +149,7 @@ function MobileWidgetSections({ key={widget.id} widget={widget} onEdit={onEditWidget ? (id) => onEditWidget(id) : undefined} + onCopy={onCopyWidget ? (id) => onCopyWidget(id) : undefined} /> ))} @@ -584,6 +591,10 @@ export function Dashboard() { setEditWidgetId(id); setWidgetDialogOpen(true); }} + onCopyWidget={(id) => { + const ref = widgetReferences.find((r) => r.widget.id === id); + if (ref) detachRef.mutate(ref.id); + }} /> ) : ( visibleWidgets.map((widget) => ( @@ -598,10 +609,12 @@ export function Dashboard() { referencedWidgetIds.has(widget.id) ? () => { // Detach: find the reference and clone it. - const ref = widgetReferences.find((r) => r.widget.id === widget.id); + const ref = widgetReferences.find( + (r) => r.widget.id === widget.id, + ); if (ref) detachRef.mutate(ref.id); } - : undefined + : undefined } /> )) diff --git a/frontend/src/pages/Settings.tsx b/frontend/src/pages/Settings.tsx index 98f355e..4eb6d78 100644 --- a/frontend/src/pages/Settings.tsx +++ b/frontend/src/pages/Settings.tsx @@ -1486,8 +1486,8 @@ function ServiceConfigEditor({
{instance.name} - - {instance.enabled ? "enabled" : "disabled"} + + {enabled ? "enabled" : "disabled"}