fix(editors): keep an edit made while a save is in flight, and land baselines per workspace

The forms stay editable during a save, and the post-write reset dropped the
local draft back to the state that was sent — so anything typed in between
vanished when the request returned. The draft is now dropped only while it
still holds what was written; an edit on top of the save survives as the
unsaved change it is, and the hosts that remount pick it back up.

A multi-workspace variable save also adopted its baselines only after every
write had succeeded, so a later workspace throwing left an earlier one's draft
dropped below a baseline that never moved: dirty forever, and retried as a
create it had already made. Each lands as its own write does.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KcyBRxE8j3ujCYkum9KrC4
This commit is contained in:
Diego Imbert
2026-09-06 03:32:53 +02:00
co-authored by Claude Opus 5
parent 8df960ebd2
commit 0635901e7f
3 changed files with 34 additions and 11 deletions
@@ -366,10 +366,14 @@
})
}
// Reset the handle to the new deployed baseline via `discard`, not
// `remove`. See VariableEditor for the full rationale.
initialStates[ws] = $state.snapshot(s) as ResourceState
// `remove`. See VariableEditor for the full rationale — including why the
// cell is only dropped while it still holds what was written.
const written = $state.snapshot(s) as ResourceState
initialStates[ws] = written
existedInitially[ws] = true
UserDraft.discard('resource', initialPath ?? '', s, { workspace: ws })
if (draftValuesEqual(states[ws]?.draft, written)) {
UserDraft.discard('resource', initialPath ?? '', written, { workspace: ws })
}
// Path now exists server-side — drop the autocomplete cache so
// it shows up immediately instead of after the 60s TTL.
invalidateWorkspacePaths(ws)
@@ -322,11 +322,22 @@
}
})
}
// Per workspace, as each write lands: a later one throwing must not leave
// this one's draft dropped below a baseline that never moved, which reads
// as dirty and retries the create it already made.
if (editPath === from) {
initialStates[ws] = s
existedInitially[ws] = true
}
// The just-saved state is the new deployed baseline; reset the
// handle to it via `discard` (not `remove` — blanking the cell to
// `undefined` reads as dirty). The `value: null` POST also deletes
// the server draft row so `is_draft` clears on refetch.
UserDraft.discard('variable', from ?? '', s, { workspace: ws })
// the server draft row so `is_draft` clears on refetch. Only while the
// cell still holds what was written: the form stays editable during the
// request, and an edit made then is a change on top of the save.
if (draftValuesEqual(states[ws]?.draft, s)) {
UserDraft.discard('variable', from ?? '', s, { workspace: ws })
}
// Path now exists server-side — drop the autocomplete cache so
// it shows up immediately instead of after the 60s TTL.
invalidateWorkspacePaths(ws)
@@ -336,12 +347,8 @@
)
dispatch('create')
// Only while this editor is still the one that was saved: re-pointed, the
// baseline and path below are the variable it moved to.
// path below is the variable it moved to.
if (editPath === from) {
for (const { ws, s } of payloads) {
initialStates[ws] = s
existedInitially[ws] = true
}
// A rename moved the item; the drawer host closes over it, but an inline one
// stays mounted, so follow the new path here and tell the host about it.
if (savedPath && savedPath !== editPath) editPath = savedPath
@@ -44,6 +44,8 @@
import { runScheduleNow } from '../scheduled/utils'
import { handleConfigChange } from '../utils'
import { withForkConflictRetry } from '$lib/utils/forkConflict'
import { deepEqual } from 'fast-equals'
import { normalizeDraftForCompare } from '$lib/userDraft.svelte'
import { useTriggerDraftSync } from '../useTriggerDraftSync.svelte'
import LocalDraftBanner from '$lib/components/LocalDraftBanner.svelte'
import TextInput from '$lib/components/text_input/TextInput.svelte'
@@ -620,7 +622,17 @@
deploymentLoading = true
const isSaved = await saveScheduleFromCfg(scheduleCfg, edit, wsId!)
if (isSaved) {
draftSync.discard(previousPath, scheduleCfg)
// Drop the local draft only while the form still holds what was saved: it
// stays editable during the request, and an edit made then is a change on
// top of the save, which the reset would silently swallow.
if (
deepEqual(
normalizeDraftForCompare($state.snapshot(getScheduleCfg())),
normalizeDraftForCompare(scheduleCfg)
)
) {
draftSync.discard(previousPath, scheduleCfg)
}
onUpdate?.(scheduleCfg.path, previousPath, previousWs)
drawer?.closeDrawer()
}