mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-08-21 08:02:26 +00:00
fix(frontend): add UserDraft.discard so "Reset to deployed" doesn't re-persist
The "Reset to deployed" toast action in resource/variable editors called UserDraft.save with the backend value to repaint the form. That left a duplicate-of-backend autosave in localStorage which would silently restore on every subsequent reload, defeating the reset. New UserDraft.discard(itemKind, path, fallback) clears LS AND resets any live handle's in-memory state to the fallback, skipping the next persist so the fallback doesn't round-trip back into storage. Backed by a new `skipNextWriteOnce()` method on useLocalStorageValue's return.
This commit is contained in:
@@ -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 })
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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 })
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -601,7 +601,7 @@ export function useLocalStorageValue<T>(
|
||||
*/
|
||||
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<T>(
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof window === 'undefined') return { val: defaultValue }
|
||||
if (typeof window === 'undefined') return { val: defaultValue, skipNextWriteOnce: () => {} }
|
||||
const savedValue = localStorage.getItem(key)
|
||||
let s = $state<T>(
|
||||
savedValue != null && savedValue !== 'undefined' ? (deserialize(savedValue) as T) : defaultValue
|
||||
@@ -694,6 +694,16 @@ export function useLocalStorageValue<T>(
|
||||
}
|
||||
}
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -89,7 +89,10 @@ function stamp<V>(stored: StoredDraft<V> | undefined): StoredDraft<V> | undefine
|
||||
return { ...stored, lastWrittenAt: Date.now() }
|
||||
}
|
||||
|
||||
type DraftState<V> = { val: StoredDraft<V> | undefined }
|
||||
type DraftState<V> = {
|
||||
val: StoredDraft<V> | 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<V>(
|
||||
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<unknown> | undefined
|
||||
}
|
||||
try {
|
||||
localStorage.removeItem(localStorageKey(ws, itemKind, path))
|
||||
} catch (e) {
|
||||
console.error('UserDraft.discard: localStorage remove failed', e)
|
||||
}
|
||||
},
|
||||
|
||||
use<V = unknown>(
|
||||
itemKind: UserDraftItemKind,
|
||||
path: string,
|
||||
|
||||
@@ -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<string>('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<string>('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<string>('flow', 'u/me/setter')
|
||||
|
||||
|
||||
Reference in New Issue
Block a user