diff --git a/frontend/src/lib/components/ResourceEditor.svelte b/frontend/src/lib/components/ResourceEditor.svelte index 6460d8fbcc..ef1bf2b0a4 100644 --- a/frontend/src/lib/components/ResourceEditor.svelte +++ b/frontend/src/lib/components/ResourceEditor.svelte @@ -216,7 +216,7 @@ if (persisted !== undefined && !deepEqual(persisted, s)) { notifyRestoredFromLocal(false, true, { onResetToDeployed: () => { - UserDraft.save('resource', initialPath ?? '', s, { workspace: ws }) + UserDraft.discard('resource', initialPath ?? '', s, { workspace: ws }) } }) } diff --git a/frontend/src/lib/components/VariableEditor.svelte b/frontend/src/lib/components/VariableEditor.svelte index 8eaea0295d..a8e53b7e91 100644 --- a/frontend/src/lib/components/VariableEditor.svelte +++ b/frontend/src/lib/components/VariableEditor.svelte @@ -137,7 +137,7 @@ if (persisted !== undefined && !deepEqual(persisted, s)) { notifyRestoredFromLocal(false, true, { onResetToDeployed: () => { - UserDraft.save('variable', p, s, { workspace: ws }) + UserDraft.discard('variable', p, s, { workspace: ws }) } }) } diff --git a/frontend/src/lib/svelte5Utils.svelte.ts b/frontend/src/lib/svelte5Utils.svelte.ts index 0bbc68cef9..e3a927281a 100644 --- a/frontend/src/lib/svelte5Utils.svelte.ts +++ b/frontend/src/lib/svelte5Utils.svelte.ts @@ -601,7 +601,7 @@ export function useLocalStorageValue( */ transformBeforePersist?: (val: T) => T } -): { val: T } { +): { val: T; skipNextWriteOnce(): void } { const saveInitialValue = options?.saveInitialValue ?? true const debounceMs = options?.debounce ?? 0 const transformBeforePersist = options?.transformBeforePersist @@ -626,7 +626,7 @@ export function useLocalStorageValue( } } - if (typeof window === 'undefined') return { val: defaultValue } + if (typeof window === 'undefined') return { val: defaultValue, skipNextWriteOnce: () => {} } const savedValue = localStorage.getItem(key) let s = $state( savedValue != null && savedValue !== 'undefined' ? (deserialize(savedValue) as T) : defaultValue @@ -694,6 +694,16 @@ export function useLocalStorageValue( } } s = newVal + }, + /** + * Arm the persist skip so the next `set val` (or deep-mutation flush) + * updates only the in-memory cell and leaves localStorage untouched. + * Used by `UserDraft.discard` to reset the in-memory state to a + * fallback without re-persisting it — pairs with an explicit LS + * delete to leave the slot empty. + */ + skipNextWriteOnce(): void { + skipNextWrite = true } } } diff --git a/frontend/src/lib/userDraft.svelte.ts b/frontend/src/lib/userDraft.svelte.ts index 03828f7d8d..fc1f2d7214 100644 --- a/frontend/src/lib/userDraft.svelte.ts +++ b/frontend/src/lib/userDraft.svelte.ts @@ -89,7 +89,10 @@ function stamp(stored: StoredDraft | undefined): StoredDraft | undefine return { ...stored, lastWrittenAt: Date.now() } } -type DraftState = { val: StoredDraft | undefined } +type DraftState = { + val: StoredDraft | undefined + skipNextWriteOnce(): void +} type DraftEntry = { count: number @@ -350,6 +353,46 @@ export const UserDraft = { } }, + /** + * Discard the local autosave and reset any live handle's `draft` to + * `fallback`. Differs from `remove` in two ways: + * + * 1. The in-memory cell is updated, so consumers reactively reading + * `handle.draft` immediately see the fallback instead of the + * stale local autosave. + * 2. The in-memory reset is marked to skip the next persist, so the + * fallback value does NOT round-trip back into localStorage. The + * LS slot stays empty until the user makes a real edit. + * + * Used by route editors' "Reset to deployed" flow: pass the backend + * baseline as `fallback` so the form repaints against deployed state + * without leaving a duplicate-of-backend LS entry behind. + */ + discard( + itemKind: UserDraftItemKind, + path: string, + fallback: V | undefined, + opts?: UserDraftOptions + ): void { + const ws = resolveWorkspace(opts) + const mk = mapKey(ws, itemKind, path) + const entry = entries.get(mk) + if (entry) { + // Arm the skip BEFORE the cell write so `useLocalStorageValue`'s + // setter consumes it and suppresses the would-be persist. The + // explicit `localStorage.removeItem` below is what actually + // clears the slot (and also covers the case where no live + // handle exists). + entry.state.skipNextWriteOnce() + entry.state.val = wrap(fallback) as StoredDraft | undefined + } + try { + localStorage.removeItem(localStorageKey(ws, itemKind, path)) + } catch (e) { + console.error('UserDraft.discard: localStorage remove failed', e) + } + }, + use( itemKind: UserDraftItemKind, path: string, diff --git a/frontend/src/lib/userDraft.test.ts b/frontend/src/lib/userDraft.test.ts index 3ea97b05c2..ae33c31587 100644 --- a/frontend/src/lib/userDraft.test.ts +++ b/frontend/src/lib/userDraft.test.ts @@ -169,6 +169,36 @@ describe('UserDraft.use() — observer sync', () => { expect(localStorage.getItem('userdraft/w/test_ws/flow/u/me/removed')).toBeNull() }) + it('discard() clears LS, resets the handle to the fallback, and does NOT re-persist', () => { + // Seed: handle holds a divergent local autosave. + localStorage.setItem('userdraft/w/test_ws/flow/u/me/discard', wrapped('local-edit')) + const handle = UserDraft.use('flow', 'u/me/discard') + expect(handle.draft).toBe('local-edit') + + // Reset to a known backend baseline. + UserDraft.discard('flow', 'u/me/discard', 'backend-baseline') + flushPersist() + + // In-memory handle reflects the fallback immediately. + expect(handle.draft).toBe('backend-baseline') + // LS is cleared and stays cleared — the fallback must NOT round-trip + // back into storage (that would make the next reload "restore" the + // fallback as if it were a real autosave). + expect(localStorage.getItem('userdraft/w/test_ws/flow/u/me/discard')).toBeNull() + }) + + it('discard() with undefined fallback clears both LS and in-memory state', () => { + localStorage.setItem('userdraft/w/test_ws/flow/u/me/wipe', wrapped('local-edit')) + const handle = UserDraft.use('flow', 'u/me/wipe') + expect(handle.draft).toBe('local-edit') + + UserDraft.discard('flow', 'u/me/wipe', undefined) + flushPersist() + + expect(handle.draft).toBeUndefined() + expect(localStorage.getItem('userdraft/w/test_ws/flow/u/me/wipe')).toBeNull() + }) + it('the second write through the handle setter persists to localStorage', () => { const handle = UserDraft.use('flow', 'u/me/setter')