From dd9cc683622d4cf4d2dcbcff0ce2df2b923c3663 Mon Sep 17 00:00:00 2001 From: Diego Imbert Date: Mon, 7 Sep 2026 19:12:26 +0200 Subject: [PATCH] fix(editors): read the live form after a save only while it is still the item's The variable and schedule editors are reused for whatever is opened next, so a drawer switched while a write is in flight leaves every post-await read describing the item now on screen. The settle then took that for an edit made over the write and left the saved item's draft standing, and the schedule also installed the saved item's baseline into the new one, which the mirror published as a draft it never had. What was sent is still adopted; the live form is read only while the editor is where it saved from. Re-pointed, the cell it released during the write is what answers for it. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01KcyBRxE8j3ujCYkum9KrC4 --- .../src/lib/components/VariableEditor.svelte | 16 ++++++-- .../schedules/ScheduleEditorInner.svelte | 41 +++++++++++-------- 2 files changed, 38 insertions(+), 19 deletions(-) diff --git a/frontend/src/lib/components/VariableEditor.svelte b/frontend/src/lib/components/VariableEditor.svelte index 4e2765b2f8..1c92f4350d 100644 --- a/frontend/src/lib/components/VariableEditor.svelte +++ b/frontend/src/lib/components/VariableEditor.svelte @@ -373,9 +373,19 @@ // to it via `discard` (not `remove` — blanking the cell to `undefined` reads // as dirty) and keeps an edit made mid-request. Each workspace settles // against the path its own write used, not the reported one. - await settleDraftAfterWrite('variable', s, states[ws]?.draft, from ?? '', s.path, { - workspace: ws - }) + // The live cell only while this editor is still on the item it saved: the + // drawer is reused, so after a re-point `states` is the next item's, and + // reading it here would take that for an edit made over this write — + // leaving this item's draft behind. Re-pointed, the cell it released + // during the write is what answers. + await settleDraftAfterWrite( + 'variable', + s, + editPath === from ? states[ws]?.draft : undefined, + from ?? '', + s.path, + { workspace: ws } + ) // Collected, not reported yet: a report retargets the host, whose `{#key // path}` would unmount this editor at the next workspace's await and leave // that one's handle released — its draft then settles against nothing. diff --git a/frontend/src/lib/components/triggers/schedules/ScheduleEditorInner.svelte b/frontend/src/lib/components/triggers/schedules/ScheduleEditorInner.svelte index 049f8292e0..31a9f5ad96 100644 --- a/frontend/src/lib/components/triggers/schedules/ScheduleEditorInner.svelte +++ b/frontend/src/lib/components/triggers/schedules/ScheduleEditorInner.svelte @@ -699,6 +699,11 @@ try { const isSaved = await saveScheduleFromCfg(scheduleCfg, edit, wsId!) if (isSaved) { + // This editor is reused for whatever is opened next, so a drawer switched + // during the write leaves every read below describing THAT schedule. Only + // what was sent may still be adopted then; the live form belongs to the + // item on screen, and its cell is not this write's to settle. + const stillHere = initialPath === previousPath && wsId === previousWs // A create deploys the schedule enabled whatever the form said (see // saveScheduleFromCfg), so what counts as written records that — the baseline // would otherwise claim a state the server does not have. The form follows @@ -706,31 +711,35 @@ if (wasCreate) { const sentEnabled = scheduleCfg.enabled scheduleCfg.enabled = true - deployedEnabled = true - if (enabled === sentEnabled) enabled = true - } else { + if (stillHere) { + deployedEnabled = true + if (enabled === sentEnabled) enabled = true + } + } else if (stillHere) { // An update's payload carries no `enabled` (see saveScheduleFromCfg), so // this write did not move it: what counts as written keeps the value the // server has accepted, and the baseline below with it. scheduleCfg.enabled = deployedEnabled } - // What was sent is the deployed value now. Adopted here rather than by - // remounting: the form stays editable during the write, and a remount would - // re-read over an edit made then — which `settleDraftAfterWrite` keeps. - initialConfig = structuredClone(scheduleCfg) - baselineNonce++ + if (stillHere) { + // What was sent is the deployed value now. Adopted here rather than by + // remounting: the form stays editable during the write, and a remount would + // re-read over an edit made then — which `settleDraftAfterWrite` keeps. + initialConfig = structuredClone(scheduleCfg) + baselineNonce++ - // The schedule is deployed now, whether it was before or not. Set here rather - // than left to the remount, which a kept edit skips: they decide whether the - // next save updates or creates, and whether a discard removes the item. - edit = true - draftOnly = false + // The schedule is deployed now, whether it was before or not. Set here rather + // than left to the remount, which a kept edit skips: they decide whether the + // next save updates or creates, and whether a discard removes the item. + edit = true + draftOnly = false + } // Awaited before the host hears: it remounts on the report, and a remount // that overtook this would load the draft this is removing. await settleDraftAfterWrite( 'trigger_schedule', scheduleCfg, - getScheduleCfg(), + stillHere ? getScheduleCfg() : undefined, previousPath, scheduleCfg.path, { workspace: previousWs ?? undefined } @@ -742,9 +751,9 @@ scheduleCfg.path, previousPath, previousWs, - !draftValuesEqual(getScheduleCfg(), scheduleCfg) + stillHere && !draftValuesEqual(getScheduleCfg(), scheduleCfg) ) - drawer?.closeDrawer() + if (stillHere) drawer?.closeDrawer() } } finally { deploymentLoading = false