mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-08-25 00:01:55 +00:00
fix(frontend): trigger autosave no longer false-restores on plain open
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -25,7 +25,7 @@
|
||||
import { saveAzureTriggerFromCfg } from './utils'
|
||||
import { getHandlerType, handleConfigChange, type Trigger } from '../utils'
|
||||
import { deepEqual } from 'fast-equals'
|
||||
import { UserDraft } from '$lib/userDraft.svelte'
|
||||
import { UserDraft, localDraftDiffers } from '$lib/userDraft.svelte'
|
||||
import { notifyRestoredFromLocal } from '$lib/userDraftToast'
|
||||
import TriggerSuspendedJobsAlert from '../TriggerSuspendedJobsAlert.svelte'
|
||||
import TriggerSuspendedJobsModal from '../TriggerSuspendedJobsModal.svelte'
|
||||
@@ -131,7 +131,7 @@
|
||||
}
|
||||
originalConfig = structuredClone($state.snapshot(getAzureConfig()))
|
||||
const localCfg = UserDraft.get<Record<string, any>>('trigger_azure', ePath)
|
||||
if (localCfg && !deepEqual(localCfg, getAzureConfig())) {
|
||||
if (localDraftDiffers(localCfg, getAzureConfig())) {
|
||||
const deployedCfg = structuredClone($state.snapshot(getAzureConfig()))
|
||||
await loadTriggerConfig(localCfg)
|
||||
notifyRestoredFromLocal(false, true, {
|
||||
@@ -314,7 +314,8 @@
|
||||
|
||||
$effect(() => {
|
||||
if (drawerLoading || !initialPath) return
|
||||
azureConfig && UserDraft.save('trigger_azure', initialPath, azureConfig)
|
||||
azureConfig &&
|
||||
UserDraft.saveIfChanged('trigger_azure', initialPath, azureConfig, originalConfig)
|
||||
})
|
||||
</script>
|
||||
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
import TriggerAdvancedBadges from '../TriggerAdvancedBadges.svelte'
|
||||
import { saveEmailTriggerFromCfg } from './utils'
|
||||
import { deepEqual } from 'fast-equals'
|
||||
import { UserDraft } from '$lib/userDraft.svelte'
|
||||
import { UserDraft, localDraftDiffers } from '$lib/userDraft.svelte'
|
||||
import { notifyRestoredFromLocal } from '$lib/userDraftToast'
|
||||
import TriggerSuspendedJobsAlert from '../TriggerSuspendedJobsAlert.svelte'
|
||||
import TriggerSuspendedJobsModal from '../TriggerSuspendedJobsModal.svelte'
|
||||
@@ -128,7 +128,7 @@
|
||||
}
|
||||
originalConfig = structuredClone($state.snapshot(getEmailTriggerConfig()))
|
||||
const localCfg = UserDraft.get<Record<string, any>>('trigger_email', ePath)
|
||||
if (localCfg && !deepEqual(localCfg, getEmailTriggerConfig())) {
|
||||
if (localDraftDiffers(localCfg, getEmailTriggerConfig())) {
|
||||
const deployedCfg = structuredClone($state.snapshot(getEmailTriggerConfig()))
|
||||
loadTriggerConfig(localCfg as Partial<EmailTrigger>)
|
||||
notifyRestoredFromLocal(false, true, {
|
||||
@@ -311,7 +311,7 @@
|
||||
|
||||
$effect(() => {
|
||||
if (drawerLoading || !initialPath) return
|
||||
UserDraft.save('trigger_email', initialPath, emailConfig)
|
||||
UserDraft.saveIfChanged('trigger_email', initialPath, emailConfig, originalConfig)
|
||||
})
|
||||
</script>
|
||||
|
||||
|
||||
@@ -27,7 +27,7 @@
|
||||
import { saveGcpTriggerFromCfg } from './utils'
|
||||
import { getHandlerType, handleConfigChange, type Trigger } from '../utils'
|
||||
import { deepEqual } from 'fast-equals'
|
||||
import { UserDraft } from '$lib/userDraft.svelte'
|
||||
import { UserDraft, localDraftDiffers } from '$lib/userDraft.svelte'
|
||||
import { notifyRestoredFromLocal } from '$lib/userDraftToast'
|
||||
import TriggerSuspendedJobsAlert from '../TriggerSuspendedJobsAlert.svelte'
|
||||
import TriggerSuspendedJobsModal from '../TriggerSuspendedJobsModal.svelte'
|
||||
@@ -133,7 +133,7 @@
|
||||
}
|
||||
originalConfig = structuredClone($state.snapshot(getGcpConfig()))
|
||||
const localCfg = UserDraft.get<Record<string, any>>('trigger_gcp', ePath)
|
||||
if (localCfg && !deepEqual(localCfg, getGcpConfig())) {
|
||||
if (localDraftDiffers(localCfg, getGcpConfig())) {
|
||||
const deployedCfg = structuredClone($state.snapshot(getGcpConfig()))
|
||||
await loadTriggerConfig(localCfg)
|
||||
notifyRestoredFromLocal(false, true, {
|
||||
@@ -331,7 +331,7 @@
|
||||
|
||||
$effect(() => {
|
||||
if (drawerLoading || !initialPath) return
|
||||
gcpConfig && UserDraft.save('trigger_gcp', initialPath, gcpConfig)
|
||||
gcpConfig && UserDraft.saveIfChanged('trigger_gcp', initialPath, gcpConfig, originalConfig)
|
||||
})
|
||||
</script>
|
||||
|
||||
|
||||
@@ -55,7 +55,7 @@
|
||||
import TriggerRetriesAndErrorHandler from '../TriggerRetriesAndErrorHandler.svelte'
|
||||
import TriggerAdvancedBadges from '../TriggerAdvancedBadges.svelte'
|
||||
import { deepEqual } from 'fast-equals'
|
||||
import { UserDraft } from '$lib/userDraft.svelte'
|
||||
import { UserDraft, localDraftDiffers } from '$lib/userDraft.svelte'
|
||||
import { notifyRestoredFromLocal } from '$lib/userDraftToast'
|
||||
import TriggerSuspendedJobsAlert from '../TriggerSuspendedJobsAlert.svelte'
|
||||
import TriggerSuspendedJobsModal from '../TriggerSuspendedJobsModal.svelte'
|
||||
@@ -227,7 +227,7 @@
|
||||
}
|
||||
originalConfig = structuredClone($state.snapshot(getRouteConfig()))
|
||||
const localCfg = UserDraft.get<Record<string, any>>('trigger_http', ePath)
|
||||
if (localCfg && !deepEqual(localCfg, getRouteConfig())) {
|
||||
if (localDraftDiffers(localCfg, getRouteConfig())) {
|
||||
const deployedCfg = structuredClone($state.snapshot(getRouteConfig()))
|
||||
loadTriggerConfig(localCfg as Partial<HttpTrigger>)
|
||||
notifyRestoredFromLocal(false, true, {
|
||||
@@ -465,7 +465,7 @@
|
||||
|
||||
$effect(() => {
|
||||
if (drawerLoading || !initialPath) return
|
||||
UserDraft.save('trigger_http', initialPath, routeConfig)
|
||||
UserDraft.saveIfChanged('trigger_http', initialPath, routeConfig, originalConfig)
|
||||
})
|
||||
</script>
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
import TriggerRetriesAndErrorHandler from '../TriggerRetriesAndErrorHandler.svelte'
|
||||
import TriggerAdvancedBadges from '../TriggerAdvancedBadges.svelte'
|
||||
import { deepEqual } from 'fast-equals'
|
||||
import { UserDraft } from '$lib/userDraft.svelte'
|
||||
import { UserDraft, localDraftDiffers } from '$lib/userDraft.svelte'
|
||||
import { notifyRestoredFromLocal } from '$lib/userDraftToast'
|
||||
import TriggerSuspendedJobsAlert from '../TriggerSuspendedJobsAlert.svelte'
|
||||
import TriggerSuspendedJobsModal from '../TriggerSuspendedJobsModal.svelte'
|
||||
@@ -155,7 +155,7 @@
|
||||
}
|
||||
originalConfig = structuredClone($state.snapshot(getSaveCfg()))
|
||||
const localCfg = UserDraft.get<Record<string, any>>('trigger_kafka', ePath)
|
||||
if (localCfg && !deepEqual(localCfg, getSaveCfg())) {
|
||||
if (localDraftDiffers(localCfg, getSaveCfg())) {
|
||||
const deployedCfg = structuredClone($state.snapshot(getSaveCfg()))
|
||||
loadTriggerConfig(localCfg)
|
||||
notifyRestoredFromLocal(false, true, {
|
||||
@@ -369,7 +369,7 @@
|
||||
|
||||
$effect(() => {
|
||||
if (drawerLoading || !initialPath) return
|
||||
UserDraft.save('trigger_kafka', initialPath, kafkaConfig)
|
||||
UserDraft.saveIfChanged('trigger_kafka', initialPath, kafkaConfig, originalConfig)
|
||||
})
|
||||
</script>
|
||||
|
||||
|
||||
@@ -39,7 +39,7 @@
|
||||
import TriggerSuspendedJobsAlert from '../TriggerSuspendedJobsAlert.svelte'
|
||||
import TriggerSuspendedJobsModal from '../TriggerSuspendedJobsModal.svelte'
|
||||
import { deepEqual } from 'fast-equals'
|
||||
import { UserDraft } from '$lib/userDraft.svelte'
|
||||
import { UserDraft, localDraftDiffers } from '$lib/userDraft.svelte'
|
||||
import { notifyRestoredFromLocal } from '$lib/userDraftToast'
|
||||
|
||||
interface Props {
|
||||
@@ -151,7 +151,7 @@
|
||||
}
|
||||
originalConfig = structuredClone($state.snapshot(getSaveCfg()))
|
||||
const localCfg = UserDraft.get<Record<string, any>>('trigger_mqtt', ePath)
|
||||
if (localCfg && !deepEqual(localCfg, getSaveCfg())) {
|
||||
if (localDraftDiffers(localCfg, getSaveCfg())) {
|
||||
const deployedCfg = structuredClone($state.snapshot(getSaveCfg()))
|
||||
await loadTriggerConfig(localCfg)
|
||||
notifyRestoredFromLocal(false, true, {
|
||||
@@ -355,7 +355,7 @@
|
||||
|
||||
$effect(() => {
|
||||
if (drawerLoading || !initialPath) return
|
||||
UserDraft.save('trigger_mqtt', initialPath, mqttConfig)
|
||||
UserDraft.saveIfChanged('trigger_mqtt', initialPath, mqttConfig, originalConfig)
|
||||
})
|
||||
</script>
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
import TriggerRetriesAndErrorHandler from '../TriggerRetriesAndErrorHandler.svelte'
|
||||
import TriggerAdvancedBadges from '../TriggerAdvancedBadges.svelte'
|
||||
import { deepEqual } from 'fast-equals'
|
||||
import { UserDraft } from '$lib/userDraft.svelte'
|
||||
import { UserDraft, localDraftDiffers } from '$lib/userDraft.svelte'
|
||||
import { notifyRestoredFromLocal } from '$lib/userDraftToast'
|
||||
import TriggerSuspendedJobsAlert from '../TriggerSuspendedJobsAlert.svelte'
|
||||
import TriggerSuspendedJobsModal from '../TriggerSuspendedJobsModal.svelte'
|
||||
@@ -139,7 +139,7 @@
|
||||
}
|
||||
originalConfig = structuredClone($state.snapshot(getSaveCfg()))
|
||||
const localCfg = UserDraft.get<Record<string, any>>('trigger_nats', ePath)
|
||||
if (localCfg && !deepEqual(localCfg, getSaveCfg())) {
|
||||
if (localDraftDiffers(localCfg, getSaveCfg())) {
|
||||
const deployedCfg = structuredClone($state.snapshot(getSaveCfg()))
|
||||
await loadTriggerConfig(localCfg)
|
||||
notifyRestoredFromLocal(false, true, {
|
||||
@@ -339,7 +339,7 @@
|
||||
|
||||
$effect(() => {
|
||||
if (drawerLoading || !initialPath) return
|
||||
UserDraft.save('trigger_nats', initialPath, natsConfig)
|
||||
UserDraft.saveIfChanged('trigger_nats', initialPath, natsConfig, originalConfig)
|
||||
})
|
||||
</script>
|
||||
|
||||
|
||||
@@ -45,7 +45,7 @@
|
||||
import TriggerSuspendedJobsAlert from '../TriggerSuspendedJobsAlert.svelte'
|
||||
import TriggerSuspendedJobsModal from '../TriggerSuspendedJobsModal.svelte'
|
||||
import { deepEqual } from 'fast-equals'
|
||||
import { UserDraft } from '$lib/userDraft.svelte'
|
||||
import { UserDraft, localDraftDiffers } from '$lib/userDraft.svelte'
|
||||
import { notifyRestoredFromLocal } from '$lib/userDraftToast'
|
||||
import { capitalize } from '$lib/utils'
|
||||
|
||||
@@ -246,7 +246,7 @@
|
||||
}
|
||||
originalConfig = structuredClone($state.snapshot(getSaveCfg()))
|
||||
const localCfg = UserDraft.get<Record<string, any>>('trigger_postgres', ePath)
|
||||
if (localCfg && !deepEqual(localCfg, getSaveCfg())) {
|
||||
if (localDraftDiffers(localCfg, getSaveCfg())) {
|
||||
const deployedCfg = structuredClone($state.snapshot(getSaveCfg()))
|
||||
await loadTriggerConfig(localCfg)
|
||||
notifyRestoredFromLocal(false, true, {
|
||||
@@ -496,7 +496,8 @@
|
||||
|
||||
$effect(() => {
|
||||
if (drawerLoading || !initialPath) return
|
||||
postgresConfig && UserDraft.save('trigger_postgres', initialPath, postgresConfig)
|
||||
postgresConfig &&
|
||||
UserDraft.saveIfChanged('trigger_postgres', initialPath, postgresConfig, originalConfig)
|
||||
})
|
||||
|
||||
$effect(() => {
|
||||
|
||||
@@ -38,8 +38,7 @@
|
||||
import { runScheduleNow } from '../scheduled/utils'
|
||||
import { handleConfigChange } from '../utils'
|
||||
import { withForkConflictRetry } from '$lib/utils/forkConflict'
|
||||
import { deepEqual } from 'fast-equals'
|
||||
import { UserDraft } from '$lib/userDraft.svelte'
|
||||
import { UserDraft, localDraftDiffers } from '$lib/userDraft.svelte'
|
||||
import { notifyRestoredFromLocal } from '$lib/userDraftToast'
|
||||
import TextInput from '$lib/components/text_input/TextInput.svelte'
|
||||
import { twMerge } from 'tailwind-merge'
|
||||
@@ -149,7 +148,7 @@
|
||||
initialConfig = structuredClone($state.snapshot(getScheduleCfg()))
|
||||
}
|
||||
const localCfg = UserDraft.get<Record<string, any>>('trigger_schedule', ePath)
|
||||
if (localCfg && !deepEqual(localCfg, getScheduleCfg())) {
|
||||
if (localDraftDiffers(localCfg, getScheduleCfg())) {
|
||||
// Snapshot the just-loaded backend config so "Reset to
|
||||
// deployed" can re-apply it, then overlay the local autosave.
|
||||
const deployedCfg = structuredClone($state.snapshot(getScheduleCfg()))
|
||||
@@ -675,7 +674,7 @@
|
||||
|
||||
$effect(() => {
|
||||
if (drawerLoading || !initialPath) return
|
||||
UserDraft.save('trigger_schedule', initialPath, scheduleCfg)
|
||||
UserDraft.saveIfChanged('trigger_schedule', initialPath, scheduleCfg, initialConfig)
|
||||
})
|
||||
</script>
|
||||
|
||||
|
||||
@@ -31,7 +31,7 @@
|
||||
import TriggerSuspendedJobsAlert from '../TriggerSuspendedJobsAlert.svelte'
|
||||
import TriggerSuspendedJobsModal from '../TriggerSuspendedJobsModal.svelte'
|
||||
import { deepEqual } from 'fast-equals'
|
||||
import { UserDraft } from '$lib/userDraft.svelte'
|
||||
import { UserDraft, localDraftDiffers } from '$lib/userDraft.svelte'
|
||||
import { notifyRestoredFromLocal } from '$lib/userDraftToast'
|
||||
|
||||
interface Props {
|
||||
@@ -137,7 +137,7 @@
|
||||
}
|
||||
originalConfig = structuredClone($state.snapshot(getSaveCfg()))
|
||||
const localCfg = UserDraft.get<Record<string, any>>('trigger_sqs', ePath)
|
||||
if (localCfg && !deepEqual(localCfg, getSaveCfg())) {
|
||||
if (localDraftDiffers(localCfg, getSaveCfg())) {
|
||||
const deployedCfg = structuredClone($state.snapshot(getSaveCfg()))
|
||||
loadTriggerConfig(localCfg)
|
||||
notifyRestoredFromLocal(false, true, {
|
||||
@@ -332,7 +332,7 @@
|
||||
// a path (no localStorage write would happen anyway).
|
||||
$effect(() => {
|
||||
if (drawerLoading || !initialPath) return
|
||||
UserDraft.save('trigger_sqs', initialPath, sqsConfig)
|
||||
UserDraft.saveIfChanged('trigger_sqs', initialPath, sqsConfig, originalConfig)
|
||||
})
|
||||
</script>
|
||||
|
||||
|
||||
@@ -42,7 +42,7 @@
|
||||
import TriggerRetriesAndErrorHandler from '../TriggerRetriesAndErrorHandler.svelte'
|
||||
import TriggerAdvancedBadges from '../TriggerAdvancedBadges.svelte'
|
||||
import { deepEqual } from 'fast-equals'
|
||||
import { UserDraft } from '$lib/userDraft.svelte'
|
||||
import { UserDraft, localDraftDiffers } from '$lib/userDraft.svelte'
|
||||
import { notifyRestoredFromLocal } from '$lib/userDraftToast'
|
||||
import TriggerSuspendedJobsAlert from '../TriggerSuspendedJobsAlert.svelte'
|
||||
import TriggerSuspendedJobsModal from '../TriggerSuspendedJobsModal.svelte'
|
||||
@@ -183,7 +183,7 @@
|
||||
}
|
||||
originalConfig = structuredClone($state.snapshot(getSaveCfg()))
|
||||
const localCfg = UserDraft.get<Record<string, any>>('trigger_websocket', ePath)
|
||||
if (localCfg && !deepEqual(localCfg, getSaveCfg())) {
|
||||
if (localDraftDiffers(localCfg, getSaveCfg())) {
|
||||
const deployedCfg = structuredClone($state.snapshot(getSaveCfg()))
|
||||
loadTriggerConfig(localCfg)
|
||||
notifyRestoredFromLocal(false, true, {
|
||||
@@ -423,7 +423,7 @@
|
||||
|
||||
$effect(() => {
|
||||
if (drawerLoading || !initialPath) return
|
||||
UserDraft.save('trigger_websocket', initialPath, websocketCfg)
|
||||
UserDraft.saveIfChanged('trigger_websocket', initialPath, websocketCfg, originalConfig)
|
||||
})
|
||||
</script>
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { get } from 'svelte/store'
|
||||
import { onDestroy, untrack } from 'svelte'
|
||||
import { deepEqual } from 'fast-equals'
|
||||
import { workspaceStore } from './stores'
|
||||
import { useLocalStorageValue } from './svelte5Utils.svelte'
|
||||
|
||||
@@ -247,6 +248,50 @@ export type UserDraftHandle<V> = {
|
||||
setMeta(meta: UserDraftMeta, opts?: { force?: boolean }): void
|
||||
}
|
||||
|
||||
/**
|
||||
* JSON round-trip normalization. localStorage persistence stringifies the
|
||||
* draft, which silently drops keys whose value is `undefined`, turns `Date`
|
||||
* into a string, etc. A freshly-built config object (e.g. a trigger editor's
|
||||
* `getXConfig()`) keeps those `undefined`-valued keys, so a raw
|
||||
* `deepEqual(persistedDraft, freshConfig)` reports spurious differences
|
||||
* (`{ a: undefined }` ≠ `{}`). Normalize BOTH sides through the same
|
||||
* round-trip before comparing. Returns the input unchanged if it can't be
|
||||
* serialized (e.g. a cyclic structure) — better a false "differs" than a
|
||||
* throw inside a load/effect path.
|
||||
*/
|
||||
export function normalizeForCompare<V>(value: V | undefined): V | undefined {
|
||||
if (value === undefined) return undefined
|
||||
try {
|
||||
return JSON.parse(JSON.stringify(value)) as V
|
||||
} catch {
|
||||
return value
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the persisted local autosave (`localDraft`, as returned by
|
||||
* `UserDraft.get`) meaningfully differs from the freshly-built
|
||||
* `currentConfig`. Editor restore guards use this to decide whether to
|
||||
* overlay the local autosave and toast.
|
||||
*
|
||||
* Returns `false` when there is no local draft. Normalizes both sides (see
|
||||
* `normalizeForCompare`) so a draft that round-trips equal to the deployed
|
||||
* config — e.g. one written by merely opening then closing the editor with
|
||||
* no edits — is correctly treated as "no meaningful draft" instead of
|
||||
* spuriously triggering a restore on every reopen.
|
||||
*
|
||||
* Typed as a guard: a `true` result narrows `localDraft` to non-nullish
|
||||
* `V`, mirroring the `localCfg && …` narrowing it replaces so call sites
|
||||
* can pass the draft straight into `loadXConfig(...)` without re-checking.
|
||||
*/
|
||||
export function localDraftDiffers<V>(
|
||||
localDraft: V | undefined | null,
|
||||
currentConfig: V
|
||||
): localDraft is V {
|
||||
if (localDraft === undefined || localDraft === null) return false
|
||||
return !deepEqual(normalizeForCompare(localDraft), normalizeForCompare(currentConfig))
|
||||
}
|
||||
|
||||
export const UserDraft = {
|
||||
save<V>(itemKind: UserDraftItemKind, path: string, value: V, opts?: UserDraftOptions): void {
|
||||
const ws = resolveWorkspace(opts)
|
||||
@@ -277,6 +322,33 @@ export const UserDraft = {
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Autosave gate. Persists `value` as the local draft only when it
|
||||
* differs (after `normalizeForCompare`) from `deployed` — the backend
|
||||
* baseline the editor loaded and keeps in sync across saves (the same
|
||||
* value the editor's own `hasChanged` / dirty signal compares against).
|
||||
* When equal, removes any existing draft instead.
|
||||
*
|
||||
* Without this, an editor's autosave `$effect` fires once as soon as it
|
||||
* finishes loading (the `drawerLoading` flag flips and the just-loaded
|
||||
* config counts as a "change" to the effect), so merely opening and
|
||||
* closing an editor with no edits would leave a no-op draft behind that
|
||||
* `has()` / restore guards then treat as unsaved work forever.
|
||||
*/
|
||||
saveIfChanged<V>(
|
||||
itemKind: UserDraftItemKind,
|
||||
path: string,
|
||||
value: V,
|
||||
deployed: V | undefined,
|
||||
opts?: UserDraftOptions
|
||||
): void {
|
||||
if (deepEqual(normalizeForCompare(value), normalizeForCompare(deployed))) {
|
||||
UserDraft.remove(itemKind, path, opts)
|
||||
} else {
|
||||
UserDraft.save(itemKind, path, value, opts)
|
||||
}
|
||||
},
|
||||
|
||||
get<V = unknown>(
|
||||
itemKind: UserDraftItemKind,
|
||||
path: string,
|
||||
|
||||
@@ -15,7 +15,8 @@ vi.mock('svelte', async (importOriginal) => {
|
||||
})
|
||||
|
||||
// Imported AFTER vi.mock so the module sees the mocked onDestroy.
|
||||
const { UserDraft, __resetUserDraftForTesting } = await import('./userDraft.svelte')
|
||||
const { UserDraft, normalizeForCompare, localDraftDiffers, __resetUserDraftForTesting } =
|
||||
await import('./userDraft.svelte')
|
||||
const { workspaceStore } = await import('./stores')
|
||||
|
||||
function flushDestroyCallbacks(): void {
|
||||
@@ -640,3 +641,81 @@ describe('gcUserDrafts', () => {
|
||||
expect(localStorage.getItem('userdraft/w/test_ws/flow/u/me/two_hours_ago')).toBeNull()
|
||||
})
|
||||
})
|
||||
|
||||
describe('normalizeForCompare', () => {
|
||||
it('returns undefined for undefined input', () => {
|
||||
expect(normalizeForCompare(undefined)).toBeUndefined()
|
||||
})
|
||||
|
||||
it('drops keys whose value is undefined (mirrors JSON.stringify persistence)', () => {
|
||||
const out = normalizeForCompare({ a: 1, b: undefined, c: { d: undefined, e: 2 } })
|
||||
expect(out).toEqual({ a: 1, c: { e: 2 } })
|
||||
expect(Object.keys(out as object)).not.toContain('b')
|
||||
expect(Object.keys((out as any).c)).not.toContain('d')
|
||||
})
|
||||
|
||||
it('falls back to the original value when not serializable (cyclic)', () => {
|
||||
const cyclic: any = { a: 1 }
|
||||
cyclic.self = cyclic
|
||||
expect(normalizeForCompare(cyclic)).toBe(cyclic)
|
||||
})
|
||||
})
|
||||
|
||||
describe('localDraftDiffers', () => {
|
||||
it('returns false when there is no local draft', () => {
|
||||
expect(localDraftDiffers(undefined, { a: 1 })).toBe(false)
|
||||
expect(localDraftDiffers(null, { a: 1 })).toBe(false)
|
||||
})
|
||||
|
||||
it('treats a draft that round-trips equal to the config as NOT differing', () => {
|
||||
// The Schedule bug: getXCfg() emits conditionally-undefined keys, but
|
||||
// the persisted draft went through JSON.stringify which dropped them.
|
||||
const freshCfg = { path: 'u/me/s', schedule: '0 0 * * *', on_failure: undefined }
|
||||
const persisted = JSON.parse(JSON.stringify(freshCfg)) // { path, schedule }
|
||||
expect(localDraftDiffers(persisted, freshCfg)).toBe(false)
|
||||
})
|
||||
|
||||
it('returns true for a genuine difference', () => {
|
||||
expect(localDraftDiffers({ a: 1 }, { a: 2 })).toBe(true)
|
||||
expect(localDraftDiffers({ a: 1, extra: 'x' }, { a: 1 })).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('UserDraft.saveIfChanged', () => {
|
||||
const KEY = 'userdraft/w/test_ws/trigger_schedule/u/me/s'
|
||||
|
||||
it('does not persist a draft equal to the deployed baseline', () => {
|
||||
const deployed = { path: 'u/me/s', schedule: '0 0 * * *', on_failure: undefined }
|
||||
// value is the post-load reactive cfg — same shape, undefined keys present
|
||||
UserDraft.saveIfChanged('trigger_schedule', 'u/me/s', { ...deployed }, deployed)
|
||||
expect(localStorage.getItem(KEY)).toBeNull()
|
||||
})
|
||||
|
||||
it('treats a value that round-trips equal to deployed as unchanged', () => {
|
||||
const deployed = { path: 'u/me/s', schedule: '0 0 * * *', on_failure: undefined }
|
||||
const value = JSON.parse(JSON.stringify(deployed)) // { path, schedule }
|
||||
UserDraft.saveIfChanged('trigger_schedule', 'u/me/s', value, deployed)
|
||||
expect(localStorage.getItem(KEY)).toBeNull()
|
||||
})
|
||||
|
||||
it('persists when the value differs from the deployed baseline', () => {
|
||||
const deployed = { path: 'u/me/s', schedule: '0 0 * * *' }
|
||||
const value = { path: 'u/me/s', schedule: '5 0 * * *' }
|
||||
UserDraft.saveIfChanged('trigger_schedule', 'u/me/s', value, deployed)
|
||||
expect(storedShape(KEY)).toBe(wrapped(value))
|
||||
})
|
||||
|
||||
it('removes a pre-existing draft once the value reverts to deployed', () => {
|
||||
const deployed = { path: 'u/me/s', schedule: '0 0 * * *' }
|
||||
UserDraft.save('trigger_schedule', 'u/me/s', { path: 'u/me/s', schedule: '5 0 * * *' })
|
||||
expect(localStorage.getItem(KEY)).not.toBeNull()
|
||||
UserDraft.saveIfChanged('trigger_schedule', 'u/me/s', { ...deployed }, deployed)
|
||||
expect(localStorage.getItem(KEY)).toBeNull()
|
||||
})
|
||||
|
||||
it('persists when there is no deployed baseline (undefined)', () => {
|
||||
const value = { path: 'u/me/s', schedule: '0 0 * * *' }
|
||||
UserDraft.saveIfChanged('trigger_schedule', 'u/me/s', value, undefined)
|
||||
expect(storedShape(KEY)).toBe(wrapped(value))
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user