Fix: dialog scroll, isDirty false positive, mobile edit btn, widget copy
Five fixes:
1. Dialog mobile scroll: DialogContent now has max-h-[calc(100dvh-2rem)]
overflow-y-auto so dialogs that don't fit on screen can scroll
instead of clipping their footer (and Cancel button) off-screen.
2. isDirty false positive: WidgetConfigDialog's SheetForm used
isDirty={draft !== null} which was true the moment you opened edit
mode, even with no changes. Now stores a draftBaseline at startEdit
time and compares JSON.stringify(draft) !== JSON.stringify(baseline).
The discard-confirmation only appears when something actually changed.
3. Mobile edit button always visible: the widget card's edit button was
opacity-0 group-hover:opacity-100 (hover-only). Changed to
md:opacity-0 md:group-hover:opacity-100 — always visible below md,
hover-reveal at md+.
4. Copy button for referenced widgets: WidgetInstanceCard gains an onCopy
prop. On the Dashboard, referenced widgets get a Copy icon button that
triggers detachRef (creates an independent clone). The edit button on
referenced widgets edits the original (shared config).
5. ConfirmDialog Cancel: fixed by #1 (the Cancel button was off-screen
on mobile dialogs that couldn't scroll).
128 tests pass; lint/build green.
This commit is contained in:
@@ -31,7 +31,7 @@ import {
|
||||
useDeleteDashboardShortcut,
|
||||
useSaveDashboardShortcut,
|
||||
} from "../hooks/useDashboard";
|
||||
import { useWidgetInstances, useWidgetReferences } from "../hooks/useWidgets";
|
||||
import { useDetachWidgetReference, useWidgetInstances, useWidgetReferences } from "../hooks/useWidgets";
|
||||
import { useServiceInstances } from "../hooks/useServices";
|
||||
import { useIsMobile } from "../hooks/useIsMobile";
|
||||
import type {
|
||||
@@ -139,11 +139,15 @@ function MobileWidgetSections({
|
||||
{SECTION_META[section.id].label}
|
||||
</h3>
|
||||
{section.widgets.map((widget) => (
|
||||
<WidgetInstanceCard key={widget.id} widget={widget} onEdit={onEditWidget ? (id) => onEditWidget(id) : undefined} />
|
||||
<WidgetInstanceCard
|
||||
key={widget.id}
|
||||
widget={widget}
|
||||
onEdit={onEditWidget ? (id) => onEditWidget(id) : undefined}
|
||||
/>
|
||||
))}
|
||||
</section>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -468,6 +472,13 @@ export function Dashboard() {
|
||||
.sort((a, b) => a.sort_order - b.sort_order);
|
||||
}, [widgetInstances, widgetReferences]);
|
||||
|
||||
// Track which visible widgets are references (for the copy/detach button).
|
||||
const referencedWidgetIds = useMemo(
|
||||
() => new Set(widgetReferences.map((r) => r.widget.id)),
|
||||
[widgetReferences],
|
||||
);
|
||||
const detachRef = useDetachWidgetReference();
|
||||
|
||||
const mobileSections = useMemo(
|
||||
() => groupWidgetsBySection(visibleWidgets, services),
|
||||
[visibleWidgets, services],
|
||||
@@ -567,10 +578,32 @@ export function Dashboard() {
|
||||
</SectionCard>
|
||||
|
||||
{isMobile && mobileSections.length > 0 ? (
|
||||
<MobileWidgetSections sections={mobileSections} onEditWidget={(id) => { setEditWidgetId(id); setWidgetDialogOpen(true); }} />
|
||||
<MobileWidgetSections
|
||||
sections={mobileSections}
|
||||
onEditWidget={(id) => {
|
||||
setEditWidgetId(id);
|
||||
setWidgetDialogOpen(true);
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
visibleWidgets.map((widget) => (
|
||||
<WidgetInstanceCard key={widget.id} widget={widget} onEdit={(id) => { setEditWidgetId(id); setWidgetDialogOpen(true); }} />
|
||||
<WidgetInstanceCard
|
||||
key={widget.id}
|
||||
widget={widget}
|
||||
onEdit={(id) => {
|
||||
setEditWidgetId(id);
|
||||
setWidgetDialogOpen(true);
|
||||
}}
|
||||
onCopy={
|
||||
referencedWidgetIds.has(widget.id)
|
||||
? () => {
|
||||
// Detach: find the reference and clone it.
|
||||
const ref = widgetReferences.find((r) => r.widget.id === widget.id);
|
||||
if (ref) detachRef.mutate(ref.id);
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
))
|
||||
)}
|
||||
|
||||
@@ -596,7 +629,10 @@ export function Dashboard() {
|
||||
/>
|
||||
<WidgetConfigDialog
|
||||
open={widgetDialogOpen}
|
||||
onClose={() => { setWidgetDialogOpen(false); setEditWidgetId(undefined); }}
|
||||
onClose={() => {
|
||||
setWidgetDialogOpen(false);
|
||||
setEditWidgetId(undefined);
|
||||
}}
|
||||
dashboardScope="main"
|
||||
editWidgetId={editWidgetId}
|
||||
/>
|
||||
|
||||
@@ -97,7 +97,14 @@ export function NamedDashboardPage() {
|
||||
{visibleWidgets.length > 0 ? (
|
||||
<div className="grid grid-cols-1 gap-4 md:grid-cols-2">
|
||||
{visibleWidgets.map((widget) => (
|
||||
<WidgetInstanceCard key={widget.id} widget={widget} onEdit={(id) => { setEditWidgetId(id); setConfigOpen(true); }} />
|
||||
<WidgetInstanceCard
|
||||
key={widget.id}
|
||||
widget={widget}
|
||||
onEdit={(id) => {
|
||||
setEditWidgetId(id);
|
||||
setConfigOpen(true);
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
) : null}
|
||||
@@ -125,7 +132,10 @@ export function NamedDashboardPage() {
|
||||
|
||||
<WidgetConfigDialog
|
||||
open={configOpen}
|
||||
onClose={() => { setConfigOpen(false); setEditWidgetId(undefined); }}
|
||||
onClose={() => {
|
||||
setConfigOpen(false);
|
||||
setEditWidgetId(undefined);
|
||||
}}
|
||||
dashboardScope={dashboardScope}
|
||||
editWidgetId={editWidgetId}
|
||||
/>
|
||||
|
||||
@@ -24,6 +24,7 @@ vi.mock("../../hooks/useSettings", () => ({
|
||||
vi.mock("../../hooks/useWidgets", () => ({
|
||||
useWidgetInstances: () => ({ data: [] }),
|
||||
useWidgetReferences: () => ({ data: [] }),
|
||||
useDetachWidgetReference: () => ({ mutate: () => {} }),
|
||||
}));
|
||||
vi.mock("../../hooks/useServices", () => ({
|
||||
useServiceInstances: () => ({ data: [] }),
|
||||
|
||||
@@ -49,7 +49,14 @@ export function OverviewTab({ instance }: { instance: ServiceInstance }) {
|
||||
{visibleWidgets.length > 0 ? (
|
||||
<div className="grid grid-cols-1 gap-4 md:grid-cols-2">
|
||||
{visibleWidgets.map((widget) => (
|
||||
<WidgetInstanceCard key={widget.id} widget={widget} onEdit={(id) => { setEditWidgetId(id); setConfigOpen(true); }} />
|
||||
<WidgetInstanceCard
|
||||
key={widget.id}
|
||||
widget={widget}
|
||||
onEdit={(id) => {
|
||||
setEditWidgetId(id);
|
||||
setConfigOpen(true);
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
@@ -72,7 +79,10 @@ export function OverviewTab({ instance }: { instance: ServiceInstance }) {
|
||||
|
||||
<WidgetConfigDialog
|
||||
open={configOpen}
|
||||
onClose={() => { setConfigOpen(false); setEditWidgetId(undefined); }}
|
||||
onClose={() => {
|
||||
setConfigOpen(false);
|
||||
setEditWidgetId(undefined);
|
||||
}}
|
||||
serviceId={instance.id}
|
||||
editWidgetId={editWidgetId}
|
||||
/>
|
||||
|
||||
Reference in New Issue
Block a user