Fix: WidgetConfigDialog scope + reorder race condition

Two bugs in the widget config dialog:

1. Service Overview edit showed main-dashboard widgets. The dialog
   called useWidgetInstances() with no args, fetching ALL widgets. Now
   accepts a serviceId prop; OverviewTab passes instance.id so the
   dialog lists + creates only service-scoped widgets. New built-in
   widgets added from a service Overview inherit the serviceId.

2. Reorder up/down buttons did nothing. moveInstance fired two
   saveWidget mutations via Promise.all — the first mutation's
   onSuccess cache invalidation triggered a refetch before the second
   completed, reverting the swap. Changed to sequential awaits so
   both sort_order writes land before the cache refreshes.

127 tests pass; lint/build green.
This commit is contained in:
Developer
2026-07-06 10:29:24 +00:00
parent 447775048c
commit bfe7ce7367
2 changed files with 10 additions and 7 deletions
@@ -38,6 +38,8 @@ import {
interface Props {
open: boolean;
onClose: () => void;
/** When set, scope the dialog to a specific service instance's widgets. */
serviceId?: string;
}
interface Draft {
@@ -164,8 +166,8 @@ function WidgetConfigEditor({
);
}
export function WidgetConfigDialog({ open, onClose }: Props) {
const { data: instances = [] } = useWidgetInstances();
export function WidgetConfigDialog({ open, onClose, serviceId }: Props) {
const { data: instances = [] } = useWidgetInstances(serviceId);
const { data: services = [] } = useServiceInstances();
const { data: tasks = [] } = useTasks();
const saveWidget = useSaveWidgetInstance();
@@ -184,7 +186,7 @@ export function WidgetConfigDialog({ open, onClose }: Props) {
function startAddBuiltIn(kind: string) {
const binding = BUILTIN_WIDGETS[kind];
setDraft({
serviceId: null,
serviceId: serviceId ?? null,
widgetKind: kind,
title: binding?.name ?? kind,
config: { ...(binding?.defaultConfig ?? {}) },
@@ -255,10 +257,10 @@ export function WidgetConfigDialog({ open, onClose }: Props) {
if (targetIndex < 0 || targetIndex >= sortedInstances.length) return;
const a = sortedInstances[index];
const b = sortedInstances[targetIndex];
await Promise.all([
saveWidget.mutateAsync({ ...a, sort_order: b.sort_order }),
saveWidget.mutateAsync({ ...b, sort_order: a.sort_order }),
]);
// Sequential (not Promise.all) to avoid a race where the first mutation's
// cache invalidation refetches before the second completes, reverting the swap.
await saveWidget.mutateAsync({ ...a, sort_order: b.sort_order });
await saveWidget.mutateAsync({ ...b, sort_order: a.sort_order });
}
async function removeInstance(instance: WidgetInstance) {