From eeb0cccbcec5034234472df9f7d055944c974a3a Mon Sep 17 00:00:00 2001 From: Developer Date: Mon, 6 Jul 2026 12:53:14 +0000 Subject: [PATCH] Add hover-reveal edit button to widget cards + auto-open edit mode Each widget card now shows a settings icon in the top-right corner on hover (desktop) or always-visible (mobile via mobile-touch-target). Clicking it opens the WidgetConfigDialog directly in edit mode for that widget (via a new editWidgetId prop on WidgetConfigDialog that auto-enters the draft-edit path via useEffect). Wired across all three widget surfaces: - Dashboard (both mobile section + desktop grid) - Service OverviewTab - NamedDashboardPage 128 tests pass; build/lint green. --- .../src/components/WidgetConfigDialog.tsx | 17 +++++- frontend/src/components/WidgetInstance.tsx | 52 +++++++++++++++---- frontend/src/pages/Dashboard.tsx | 14 +++-- frontend/src/pages/NamedDashboardPage.tsx | 6 ++- .../src/pages/service-tabs/OverviewTab.tsx | 6 ++- 5 files changed, 74 insertions(+), 21 deletions(-) diff --git a/frontend/src/components/WidgetConfigDialog.tsx b/frontend/src/components/WidgetConfigDialog.tsx index 52da72d..801829f 100644 --- a/frontend/src/components/WidgetConfigDialog.tsx +++ b/frontend/src/components/WidgetConfigDialog.tsx @@ -1,4 +1,4 @@ -import { useMemo, useState } from "react"; +import { useEffect, useMemo, useState } from "react"; import { Dialog, DialogContent, @@ -56,6 +56,8 @@ interface Props { serviceId?: string; /** When set, enable widget references ("Add existing") for this dashboard scope. */ dashboardScope?: string; + /** When set, auto-open in edit mode for this widget id (instead of the list view). */ + editWidgetId?: string; } interface Draft { @@ -203,6 +205,7 @@ export function WidgetConfigDialog({ onClose, serviceId, dashboardScope, + editWidgetId, }: Props) { const { data: instances = [] } = useWidgetInstances(serviceId); const { data: services = [] } = useServiceInstances(); @@ -220,6 +223,18 @@ export function WidgetConfigDialog({ const [draft, setDraft] = useState(null); + // When editWidgetId is set and the dialog opens, auto-enter edit mode for + // that widget (instead of showing the list view). + useEffect(() => { + if (open && editWidgetId && instances.length > 0) { + const target = instances.find((w) => w.id === editWidgetId); + if (target) { + startEdit(target); + } + } + + }, [open, editWidgetId, instances]); + function startAddBuiltIn(kind: string) { const binding = BUILTIN_WIDGETS[kind]; setDraft({ diff --git a/frontend/src/components/WidgetInstance.tsx b/frontend/src/components/WidgetInstance.tsx index d0e934b..e3d80cb 100644 --- a/frontend/src/components/WidgetInstance.tsx +++ b/frontend/src/components/WidgetInstance.tsx @@ -1,4 +1,6 @@ import { Alert, AlertDescription } from "@/components/ui/alert"; +import { Button } from "@/components/ui/button"; +import { Settings2 } from "lucide-react"; import { useServiceInstances } from "../hooks/useServices"; import { resolveWidget } from "../integrations/registry"; import type { WidgetInstance } from "../types"; @@ -6,9 +8,11 @@ import { SectionCard } from "./SectionCard"; interface Props { widget: WidgetInstance; + /** When provided, a hover-reveal edit button appears in the top-right corner. */ + onEdit?: (widgetId: string) => void; } -export function WidgetInstanceCard({ widget }: Props) { +export function WidgetInstanceCard({ widget, onEdit }: Props) { const { data: services = [] } = useServiceInstances(); const resolved = resolveWidget(widget, services); @@ -17,20 +21,46 @@ export function WidgetInstanceCard({ widget }: Props) { ? `Unknown widget: ${widget.widget_kind} (service-bound)` : `Unknown widget: ${widget.widget_kind} (built-in)`; return ( - - - {label} - - +
+ {onEdit ? ( + + ) : null} + + + {label} + + +
); } const Component = resolved.component; return ( - +
+ {onEdit ? ( + + ) : null} + +
); } diff --git a/frontend/src/pages/Dashboard.tsx b/frontend/src/pages/Dashboard.tsx index 788c630..31de91e 100644 --- a/frontend/src/pages/Dashboard.tsx +++ b/frontend/src/pages/Dashboard.tsx @@ -97,8 +97,10 @@ function groupWidgetsBySection( function MobileWidgetSections({ sections, + onEditWidget, }: { sections: { id: SectionId; widgets: WidgetInstance[] }[]; + onEditWidget?: (widgetId: string) => void; }) { return ( <> @@ -137,11 +139,11 @@ function MobileWidgetSections({ {SECTION_META[section.id].label} {section.widgets.map((widget) => ( - + onEditWidget(id) : undefined} /> ))} ))} - + ); } @@ -448,6 +450,7 @@ export function Dashboard() { ); const [deleteShortcutId, setDeleteShortcutId] = useState(null); const [widgetDialogOpen, setWidgetDialogOpen] = useState(false); + const [editWidgetId, setEditWidgetId] = useState(); const { data: widgetInstances = [] } = useWidgetInstances( undefined, "dashboard", @@ -564,10 +567,10 @@ export function Dashboard() { {isMobile && mobileSections.length > 0 ? ( - + { setEditWidgetId(id); setWidgetDialogOpen(true); }} /> ) : ( visibleWidgets.map((widget) => ( - + { setEditWidgetId(id); setWidgetDialogOpen(true); }} /> )) )} @@ -593,8 +596,9 @@ export function Dashboard() { /> setWidgetDialogOpen(false)} + onClose={() => { setWidgetDialogOpen(false); setEditWidgetId(undefined); }} dashboardScope="main" + editWidgetId={editWidgetId} /> ); diff --git a/frontend/src/pages/NamedDashboardPage.tsx b/frontend/src/pages/NamedDashboardPage.tsx index ad3b28d..db848b6 100644 --- a/frontend/src/pages/NamedDashboardPage.tsx +++ b/frontend/src/pages/NamedDashboardPage.tsx @@ -49,6 +49,7 @@ export function NamedDashboardPage() { const dashboardScope = `named:${slug}`; const { data: widgetRefs = [] } = useWidgetReferences(dashboardScope); const [configOpen, setConfigOpen] = useState(false); + const [editWidgetId, setEditWidgetId] = useState(); const items = useMemo( () => parseItems(dashboard?.payload ?? {}), @@ -96,7 +97,7 @@ export function NamedDashboardPage() { {visibleWidgets.length > 0 ? (
{visibleWidgets.map((widget) => ( - + { setEditWidgetId(id); setConfigOpen(true); }} /> ))}
) : null} @@ -124,8 +125,9 @@ export function NamedDashboardPage() { setConfigOpen(false)} + onClose={() => { setConfigOpen(false); setEditWidgetId(undefined); }} dashboardScope={dashboardScope} + editWidgetId={editWidgetId} /> ); diff --git a/frontend/src/pages/service-tabs/OverviewTab.tsx b/frontend/src/pages/service-tabs/OverviewTab.tsx index 4c54394..3a2dc04 100644 --- a/frontend/src/pages/service-tabs/OverviewTab.tsx +++ b/frontend/src/pages/service-tabs/OverviewTab.tsx @@ -19,6 +19,7 @@ import type { ServiceInstance } from "../../types"; export function OverviewTab({ instance }: { instance: ServiceInstance }) { const { data: widgets = [] } = useWidgetInstances(instance.id); const [configOpen, setConfigOpen] = useState(false); + const [editWidgetId, setEditWidgetId] = useState(); const visibleWidgets = useMemo( () => @@ -48,7 +49,7 @@ export function OverviewTab({ instance }: { instance: ServiceInstance }) { {visibleWidgets.length > 0 ? (
{visibleWidgets.map((widget) => ( - + { setEditWidgetId(id); setConfigOpen(true); }} /> ))}
) : ( @@ -71,8 +72,9 @@ export function OverviewTab({ instance }: { instance: ServiceInstance }) { setConfigOpen(false)} + onClose={() => { setConfigOpen(false); setEditWidgetId(undefined); }} serviceId={instance.id} + editWidgetId={editWidgetId} /> );