feat: add 'Reset to deployed' action on draft-loaded toast

This commit is contained in:
Diego Imbert
2026-06-03 10:12:28 +02:00
parent 79cb813be7
commit 4ebac0ee4c
18 changed files with 247 additions and 22 deletions
@@ -5,6 +5,7 @@
import { createEventDispatcher, untrack } from 'svelte'
import { userStore, workspaceStore } from '$lib/stores'
import { sendUserToast } from '$lib/toast'
import { notifyDraftLoaded } from '$lib/userDraftToast'
import { clearJsonSchemaResourceCache } from './schema/jsonSchemaResource.svelte'
import ResourceForm from './ResourceForm.svelte'
import { invalidateWorkspacePaths } from './PathNameAutocomplete.svelte'
@@ -254,7 +255,29 @@
getUserExt(ws)
]).then(([r, user]) => {
if (r.is_draft) {
sendUserToast('Loaded your saved draft')
notifyDraftLoaded({
workspace: ws,
itemKind: 'resource',
path: initialPath ?? '',
onResetToDeployed: async () => {
const fresh = await ResourceService.getResource({
workspace: ws,
path: initialPath ?? ''
})
const deployed: ResourceState = {
path: fresh.path,
description: fresh.description ?? '',
args: (fresh.value ?? {}) as any,
labels: fresh.labels ?? undefined,
wsSpecific: fresh.ws_specific ?? false
}
fetchedResources[ws] = fresh
fetchedRev[ws] = fresh.edited_at
initialStates[ws] = structuredClone(deployed)
const handle = states[ws]
if (handle) handle.setDraftAndMeta(undefined, { remoteRev: fresh.edited_at })
}
})
}
fetchedResources[ws] = r
fetchedRev[ws] = r.edited_at
@@ -7,6 +7,7 @@
import DrawerContent from './common/drawer/DrawerContent.svelte'
import Alert from './common/alert/Alert.svelte'
import { sendUserToast } from '$lib/toast'
import { notifyDraftLoaded } from '$lib/userDraftToast'
import { canWrite } from '$lib/utils'
import { Save } from 'lucide-svelte'
import VariableForm from './VariableForm.svelte'
@@ -168,7 +169,32 @@
getUserExt(ws)
]).then(([v, user]) => {
if (v.is_draft) {
sendUserToast('Loaded your saved draft')
notifyDraftLoaded({
workspace: ws,
itemKind: 'variable',
path: p,
onResetToDeployed: async () => {
const fresh = await VariableService.getVariable({
workspace: ws,
path: p,
decryptSecret: false
})
const deployed: VariableState = {
path: fresh.path,
variable: {
value: fresh.value ?? '',
is_secret: fresh.is_secret,
description: fresh.description ?? ''
},
labels: fresh.labels ?? undefined,
wsSpecific: fresh.ws_specific ?? false
}
fetchedRev[ws] = fresh.edited_at
initialStates[ws] = structuredClone(deployed)
const handle = states[ws]
if (handle) handle.setDraftAndMeta(undefined, { remoteRev: fresh.edited_at })
}
})
}
fetchedRev[ws] = v.edited_at
const s: VariableState = {
@@ -5,6 +5,7 @@
import Path from '$lib/components/Path.svelte'
import { usedTriggerKinds, userStore, workspaceStore } from '$lib/stores'
import { canWrite, capitalize, emptyString, sendUserToast } from '$lib/utils'
import { notifyDraftLoaded } from '$lib/userDraftToast'
import { withForkConflictRetry } from '$lib/utils/forkConflict'
import { Loader2 } from 'lucide-svelte'
import Label from '$lib/components/Label.svelte'
@@ -196,7 +197,14 @@
getDraft: true
})
if (s?.is_draft) {
sendUserToast('Loaded your saved draft')
notifyDraftLoaded({
workspace: $workspaceStore!,
itemKind: 'trigger_azure',
path: initialPath,
onResetToDeployed: async () => {
await loadTrigger()
}
})
}
loadTriggerConfig(s)
} catch (error) {
@@ -15,6 +15,7 @@
} from '$lib/gen'
import { usedTriggerKinds, userStore, workspaceStore } from '$lib/stores'
import { canWrite, capitalize, emptyString, sendUserToast } from '$lib/utils'
import { notifyDraftLoaded } from '$lib/userDraftToast'
import Section from '$lib/components/Section.svelte'
import { Loader2 } from 'lucide-svelte'
import Label from '$lib/components/Label.svelte'
@@ -217,7 +218,14 @@
getDraft: true
})
if (s?.is_draft) {
sendUserToast('Loaded your saved draft')
notifyDraftLoaded({
workspace: $workspaceStore!,
itemKind: 'trigger_email',
path: initialPath,
onResetToDeployed: async () => {
await loadTrigger()
}
})
}
loadTriggerConfig(s)
@@ -5,6 +5,7 @@
import Path from '$lib/components/Path.svelte'
import { usedTriggerKinds, userStore, workspaceStore } from '$lib/stores'
import { canWrite, capitalize, emptyString, sendUserToast } from '$lib/utils'
import { notifyDraftLoaded } from '$lib/userDraftToast'
import { withForkConflictRetry } from '$lib/utils/forkConflict'
import { Loader2 } from 'lucide-svelte'
import Label from '$lib/components/Label.svelte'
@@ -200,7 +201,14 @@
getDraft: true
})
if (s?.is_draft) {
sendUserToast('Loaded your saved draft')
notifyDraftLoaded({
workspace: $workspaceStore!,
itemKind: 'trigger_gcp',
path: initialPath,
onResetToDeployed: async () => {
await loadTrigger()
}
})
}
loadTriggerConfig(s)
} catch (error) {
@@ -23,6 +23,7 @@
generateRandomString,
sendUserToast
} from '$lib/utils'
import { notifyDraftLoaded } from '$lib/userDraftToast'
import Section from '$lib/components/Section.svelte'
import { Loader2, Pipette, Plus } from 'lucide-svelte'
import Label from '$lib/components/Label.svelte'
@@ -350,7 +351,14 @@
getDraft: true
})
if (s?.is_draft) {
sendUserToast('Loaded your saved draft')
notifyDraftLoaded({
workspace: $workspaceStore!,
itemKind: 'trigger_http',
path: initialPath,
onResetToDeployed: async () => {
await loadTrigger()
}
})
}
loadTriggerConfig(s)
@@ -9,6 +9,7 @@
import { KafkaTriggerService, type ErrorHandler, type Retry, type TriggerMode } from '$lib/gen'
import { usedTriggerKinds, userStore, workspaceStore } from '$lib/stores'
import { canWrite, capitalize, emptyString, sendUserToast } from '$lib/utils'
import { notifyDraftLoaded } from '$lib/userDraftToast'
import { withForkConflictRetry } from '$lib/utils/forkConflict'
import Section from '$lib/components/Section.svelte'
import { Loader2, RotateCcw } from 'lucide-svelte'
@@ -256,7 +257,14 @@
getDraft: true
})
if (s?.is_draft) {
sendUserToast('Loaded your saved draft')
notifyDraftLoaded({
workspace: $workspaceStore!,
itemKind: 'trigger_kafka',
path: initialPath,
onResetToDeployed: async () => {
await loadTrigger()
}
})
}
loadTriggerConfig(s)
}
@@ -8,6 +8,7 @@
import ScriptPicker from '$lib/components/ScriptPicker.svelte'
import { usedTriggerKinds, userStore, workspaceStore } from '$lib/stores'
import { canWrite, capitalize, emptyString, sendUserToast } from '$lib/utils'
import { notifyDraftLoaded } from '$lib/userDraftToast'
import { withForkConflictRetry } from '$lib/utils/forkConflict'
import Section from '$lib/components/Section.svelte'
import { Loader2 } from 'lucide-svelte'
@@ -256,7 +257,14 @@
getDraft: true
})
if (s?.is_draft) {
sendUserToast('Loaded your saved draft')
notifyDraftLoaded({
workspace: $workspaceStore!,
itemKind: 'trigger_mqtt',
path: initialPath,
onResetToDeployed: async () => {
await loadTrigger()
}
})
}
loadTriggerConfig(s)
}
@@ -8,6 +8,7 @@
import { NatsTriggerService, type ErrorHandler, type Retry, type TriggerMode } from '$lib/gen'
import { usedTriggerKinds, userStore, workspaceStore } from '$lib/stores'
import { canWrite, capitalize, emptyString, sendUserToast } from '$lib/utils'
import { notifyDraftLoaded } from '$lib/userDraftToast'
import { withForkConflictRetry } from '$lib/utils/forkConflict'
import Section from '$lib/components/Section.svelte'
import { Loader2 } from 'lucide-svelte'
@@ -238,7 +239,14 @@
getDraft: true
})
if (s?.is_draft) {
sendUserToast('Loaded your saved draft')
notifyDraftLoaded({
workspace: $workspaceStore!,
itemKind: 'trigger_nats',
path: initialPath,
onResetToDeployed: async () => {
await loadTrigger()
}
})
}
loadTriggerConfig(s)
}
@@ -15,6 +15,7 @@
} from '$lib/gen'
import { usedTriggerKinds, userStore, workspaceStore } from '$lib/stores'
import { canWrite, emptyString, emptyStringTrimmed, sendUserToast } from '$lib/utils'
import { notifyDraftLoaded } from '$lib/userDraftToast'
import { withForkConflictRetry } from '$lib/utils/forkConflict'
import Section from '$lib/components/Section.svelte'
import { Loader2 } from 'lucide-svelte'
@@ -382,7 +383,14 @@
getDraft: true
})
if (s?.is_draft) {
sendUserToast('Loaded your saved draft')
notifyDraftLoaded({
workspace: $workspaceStore!,
itemKind: 'trigger_postgres',
path: initialPath,
onResetToDeployed: async () => {
await loadTrigger()
}
})
}
const publication_data = await PostgresTriggerService.getPostgresPublication({
@@ -25,6 +25,7 @@
} from '$lib/gen'
import { enterpriseLicense, userStore, workspaceStore } from '$lib/stores'
import { canWrite, emptyString, formatCron, sendUserToast, cronV1toV2 } from '$lib/utils'
import { notifyDraftLoaded } from '$lib/userDraftToast'
import { base } from '$lib/base'
import Section from '$lib/components/Section.svelte'
import { List, Loader2, Save, AlertTriangle } from 'lucide-svelte'
@@ -292,7 +293,14 @@
getDraft: true
})
if (resp.is_draft) {
sendUserToast('Loaded your saved draft')
notifyDraftLoaded({
workspace: $workspaceStore!,
itemKind: 'trigger_schedule',
path: schedule_path,
onResetToDeployed: async () => {
await openNew(nis_flow, initial_script_path, defaultValues, schedule_path)
}
})
}
s = resp
initNewPath = true
@@ -466,7 +474,14 @@
getDraft: true
})
if (s.is_draft) {
sendUserToast('Loaded your saved draft')
notifyDraftLoaded({
workspace: $workspaceStore!,
itemKind: 'trigger_schedule',
path: initialPath,
onResetToDeployed: async () => {
await loadSchedule()
}
})
}
await loadScheduleCfg(s)
} catch (err) {
@@ -5,6 +5,7 @@
import Path from '$lib/components/Path.svelte'
import { usedTriggerKinds, userStore, workspaceStore } from '$lib/stores'
import { canWrite, capitalize, emptyString, sendUserToast } from '$lib/utils'
import { notifyDraftLoaded } from '$lib/userDraftToast'
import { withForkConflictRetry } from '$lib/utils/forkConflict'
import { Loader2 } from 'lucide-svelte'
import Label from '$lib/components/Label.svelte'
@@ -233,7 +234,14 @@
getDraft: true
})
if (s?.is_draft) {
sendUserToast('Loaded your saved draft')
notifyDraftLoaded({
workspace: $workspaceStore!,
itemKind: 'trigger_sqs',
path: initialPath,
onResetToDeployed: async () => {
await loadTrigger()
}
})
}
loadTriggerConfig(s)
}
@@ -21,6 +21,7 @@
} from '$lib/gen'
import { usedTriggerKinds, userStore, workspaceStore } from '$lib/stores'
import { canWrite, emptySchema, emptyString, sendUserToast } from '$lib/utils'
import { notifyDraftLoaded } from '$lib/userDraftToast'
import { withForkConflictRetry } from '$lib/utils/forkConflict'
import Section from '$lib/components/Section.svelte'
import { Loader2, X, Plus } from 'lucide-svelte'
@@ -313,7 +314,14 @@
getDraft: true
})
if (s?.is_draft) {
sendUserToast('Loaded your saved draft')
notifyDraftLoaded({
workspace: $workspaceStore!,
itemKind: 'trigger_websocket',
path: initialPath,
onResetToDeployed: async () => {
await loadTrigger()
}
})
}
loadTriggerConfig(s)
}
+44
View File
@@ -5,6 +5,8 @@
* site (route-specific state).
*/
import { sendUserToast } from '$lib/toast'
import { UserDraftDbSyncer } from '$lib/userDraftDbSyncer.svelte'
import type { UserDraftItemKind } from '$lib/gen'
export type RestoreFromLocalActions = {
/** Drop the local autosave, apply the backend DB draft. Offered when `hasBackendDraft`. */
@@ -29,3 +31,45 @@ export function notifyRestoredFromLocal(
if (actions.length === 0) return
sendUserToast('Restored from local storage', false, actions)
}
/**
* Shown when an editor mounts on a per-user draft fetched from the server
* (the `get_draft=true` overlay returned `is_draft: true`). Offers a
* "Reset to deployed" action that:
* 1. POSTs `value: null` to delete the user's draft for this entity, and
* 2. invokes `onResetToDeployed` so the editor can re-fetch without the
* overlay and apply the pure deployed version.
*
* Failures in either step surface a follow-up toast and leave the editor
* state untouched.
*/
export function notifyDraftLoaded(opts: {
workspace: string
itemKind: UserDraftItemKind
path: string
onResetToDeployed: () => void | Promise<void>
}): void {
sendUserToast('Loaded your saved draft', false, [
{
label: 'Reset to deployed',
callback: async () => {
try {
await UserDraftDbSyncer.save({
workspace: opts.workspace,
itemKind: opts.itemKind,
path: opts.path,
value: null
})
} catch (e: any) {
sendUserToast(`Could not delete draft: ${e?.body ?? e}`, true)
return
}
try {
await opts.onResetToDeployed()
} catch (e: any) {
sendUserToast(`Could not reset to deployed: ${e?.body ?? e}`, true)
}
}
}
])
}
@@ -15,7 +15,7 @@
import { untrack } from 'svelte'
import { page } from '$app/state'
import { UserDraft, checkStaleness, type UserDraftMeta } from '$lib/userDraft.svelte'
import { notifyRestoredFromLocal } from '$lib/userDraftToast'
import { notifyDraftLoaded, notifyRestoredFromLocal } from '$lib/userDraftToast'
let app = $state(undefined as (AppWithLastVersion & { value: any }) | undefined)
let savedApp:
@@ -119,7 +119,16 @@
})
if (tok !== loadAppToken) return
if (backendApp.is_draft) {
sendUserToast('Loaded your saved draft')
notifyDraftLoaded({
workspace: $workspaceStore!,
itemKind: 'app',
path: page.params.path ?? '',
onResetToDeployed: async () => {
UserDraft.remove('app', path)
await loadApp()
redraw++
}
})
}
const backendApp_ = structuredClone(stateSnapshot(backendApp))
savedApp = {
@@ -19,7 +19,7 @@
localDraftDiffers,
type UserDraftMeta
} from '$lib/userDraft.svelte'
import { notifyRestoredFromLocal } from '$lib/userDraftToast'
import { notifyDraftLoaded, notifyRestoredFromLocal } from '$lib/userDraftToast'
import LocalDraftStaleModal from '$lib/components/common/confirmationModal/LocalDraftStaleModal.svelte'
import OtherUsersDraftsModal from '$lib/components/common/confirmationModal/OtherUsersDraftsModal.svelte'
@@ -199,7 +199,15 @@
})) as any
if (tok !== loadAppToken) return
if (backendApp.is_draft) {
sendUserToast('Loaded your saved draft')
notifyDraftLoaded({
workspace: $workspaceStore!,
itemKind: 'raw_app',
path: page.params.path ?? '',
onResetToDeployed: async () => {
draftHandle.setDraftAndMeta(undefined, {})
await loadApp()
}
})
}
// When the backend falls back to `fetch_draft_only` (no deployed
// row at this path, only a per-user draft), the response's inner
@@ -29,7 +29,7 @@
type UserDraftMeta,
type UserDraftHandle
} from '$lib/userDraft.svelte'
import { notifyRestoredFromLocal } from '$lib/userDraftToast'
import { notifyDraftLoaded, notifyRestoredFromLocal } from '$lib/userDraftToast'
let version: undefined | number = $state(undefined)
@@ -218,7 +218,15 @@
})
if (tok !== loadFlowToken) return
if (backendFlow.is_draft) {
sendUserToast('Loaded your saved draft')
notifyDraftLoaded({
workspace: $workspaceStore!,
itemKind: 'flow',
path: page.params.path ?? '',
onResetToDeployed: async () => {
flowHandle.setDraftAndMeta(undefined, {})
await loadFlow()
}
})
}
savedFlow = structuredClone($state.snapshot(backendFlow)) as Flow
@@ -21,7 +21,7 @@
type UserDraftMeta,
type UserDraftHandle
} from '$lib/userDraft.svelte'
import { notifyRestoredFromLocal } from '$lib/userDraftToast'
import { notifyDraftLoaded, notifyRestoredFromLocal } from '$lib/userDraftToast'
type EditableScript = NewScript & { draft_triggers?: Trigger[] }
@@ -183,7 +183,19 @@
})
if (tok !== loadScriptToken) return
if (backendScript.is_draft) {
sendUserToast('Loaded your saved draft')
notifyDraftLoaded({
workspace: $workspaceStore!,
itemKind: 'script',
path: page.params.path ?? '',
onResetToDeployed: async () => {
// Drop the in-memory draft so loadScript's no-localDraft
// branch fires; the next fetch with `getDraft: true` will
// return pure deployed because we just deleted the draft
// row server-side.
scriptHandle.setDraftAndMeta(undefined, {})
await loadScript()
}
})
}
savedScript = structuredClone($state.snapshot(backendScript))