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 { interface Props {
open: boolean; open: boolean;
onClose: () => void; onClose: () => void;
/** When set, scope the dialog to a specific service instance's widgets. */
serviceId?: string;
} }
interface Draft { interface Draft {
@@ -164,8 +166,8 @@ function WidgetConfigEditor({
); );
} }
export function WidgetConfigDialog({ open, onClose }: Props) { export function WidgetConfigDialog({ open, onClose, serviceId }: Props) {
const { data: instances = [] } = useWidgetInstances(); const { data: instances = [] } = useWidgetInstances(serviceId);
const { data: services = [] } = useServiceInstances(); const { data: services = [] } = useServiceInstances();
const { data: tasks = [] } = useTasks(); const { data: tasks = [] } = useTasks();
const saveWidget = useSaveWidgetInstance(); const saveWidget = useSaveWidgetInstance();
@@ -184,7 +186,7 @@ export function WidgetConfigDialog({ open, onClose }: Props) {
function startAddBuiltIn(kind: string) { function startAddBuiltIn(kind: string) {
const binding = BUILTIN_WIDGETS[kind]; const binding = BUILTIN_WIDGETS[kind];
setDraft({ setDraft({
serviceId: null, serviceId: serviceId ?? null,
widgetKind: kind, widgetKind: kind,
title: binding?.name ?? kind, title: binding?.name ?? kind,
config: { ...(binding?.defaultConfig ?? {}) }, config: { ...(binding?.defaultConfig ?? {}) },
@@ -255,10 +257,10 @@ export function WidgetConfigDialog({ open, onClose }: Props) {
if (targetIndex < 0 || targetIndex >= sortedInstances.length) return; if (targetIndex < 0 || targetIndex >= sortedInstances.length) return;
const a = sortedInstances[index]; const a = sortedInstances[index];
const b = sortedInstances[targetIndex]; const b = sortedInstances[targetIndex];
await Promise.all([ // Sequential (not Promise.all) to avoid a race where the first mutation's
saveWidget.mutateAsync({ ...a, sort_order: b.sort_order }), // cache invalidation refetches before the second completes, reverting the swap.
saveWidget.mutateAsync({ ...b, sort_order: a.sort_order }), await saveWidget.mutateAsync({ ...a, sort_order: b.sort_order });
]); await saveWidget.mutateAsync({ ...b, sort_order: a.sort_order });
} }
async function removeInstance(instance: WidgetInstance) { async function removeInstance(instance: WidgetInstance) {
@@ -72,6 +72,7 @@ export function OverviewTab({ instance }: { instance: ServiceInstance }) {
<WidgetConfigDialog <WidgetConfigDialog
open={configOpen} open={configOpen}
onClose={() => setConfigOpen(false)} onClose={() => setConfigOpen(false)}
serviceId={instance.id}
/> />
</div> </div>
); );