From c479afab8ebceccbee050e923dc5c27a6712ea62 Mon Sep 17 00:00:00 2001 From: Ruben Fiszel Date: Sun, 28 Jun 2026 11:25:59 +0200 Subject: [PATCH] fix: redeploy older app version from deployment history (#9826) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: redeploy older app version from deployment history Co-Authored-By: Claude Opus 4.8 (1M context) * fix: apply restored app version to low-code editor on redeploy Redeploying an older app version from Deployment History fired the restore callback (toast shown) but the canvas kept displaying the current version, and Deploy then shipped that current value. AppEditor seeds its working state from `appDraftHandle.draft ?? app`, preferring the per-path autosave over the freshly restored `app` prop. The remount triggered by the restore therefore re-read the stale pre-restore draft. `reloadDeployed` already clears the draft before remounting for the reset-to-deployed flow; `onRestore` was missing the same step. Drop the autosave in `onRestore` so the remounted editor seeds from the restored value. Raw apps are unaffected: RawAppEditor binds `files` directly (no draft precedence), and `extractRawApp` mutates that bound state in place. Co-Authored-By: Claude Opus 4.8 (1M context) * fix(raw-apps): convert savedNewAppPath event forwarding to a callback prop `svelte-check` (CI `npm check`) failed with one error: forwarding the `savedNewAppPath` createEventDispatcher event through the runes-mode RawAppEditor → RawAppEditorHeader chain types as "not assignable to never". This is the same legacy-forwarding-through-runes pattern already removed for `restore` in this PR — `on:savedNewAppPath` would likewise be dropped at runtime, breaking navigation to the new path after a deploy that renames the app. Replace the `on:savedNewAppPath` forwarding with an `onSavedNewAppPath` callback prop threaded page → RawAppEditor → RawAppEditorHeader, matching `onRestore`. The header now invokes the callback instead of dispatching, and its now-unused createEventDispatcher is removed. Co-Authored-By: Claude Opus 4.8 (1M context) --------- Co-authored-by: Claude Opus 4.8 (1M context) --- .../components/apps/editor/AppEditor.svelte | 5 +++-- .../apps/editor/AppEditorHeader.svelte | 9 ++++++-- frontend/src/lib/components/apps/types.ts | 4 ++++ .../components/raw_apps/RawAppEditor.svelte | 16 +++++++++++--- .../raw_apps/RawAppEditorHeader.svelte | 22 +++++++++++++------ .../(logged)/apps/edit/[...path]/+page.svelte | 11 +++++++--- .../apps_raw/edit/[...path]/+page.svelte | 12 +++++----- 7 files changed, 56 insertions(+), 23 deletions(-) diff --git a/frontend/src/lib/components/apps/editor/AppEditor.svelte b/frontend/src/lib/components/apps/editor/AppEditor.svelte index 0cffdc2400..f6e3245c2c 100644 --- a/frontend/src/lib/components/apps/editor/AppEditor.svelte +++ b/frontend/src/lib/components/apps/editor/AppEditor.svelte @@ -83,7 +83,8 @@ onResetToDeployed, loadedFromDraft = false, othersDraftsCount = 0, - onOpenOthersDrafts + onOpenOthersDrafts, + onRestore }: AppEditorProps = $props() migrateApp(untrack(() => app)) @@ -890,7 +891,7 @@ {loadedFromDraft} {othersDraftsCount} {onOpenOthersDrafts} - on:restore + {onRestore} {policy} {fromHub} bind:this={appEditorHeader} diff --git a/frontend/src/lib/components/apps/editor/AppEditorHeader.svelte b/frontend/src/lib/components/apps/editor/AppEditorHeader.svelte index 1de186c301..0ed389f52c 100644 --- a/frontend/src/lib/components/apps/editor/AppEditorHeader.svelte +++ b/frontend/src/lib/components/apps/editor/AppEditorHeader.svelte @@ -112,6 +112,10 @@ loadedFromDraft?: boolean othersDraftsCount?: number onOpenOthersDrafts?: () => void + // Restoring an older deployment from the history drawer. A callback prop + // (not `on:restore` forwarding): forwarding a `createEventDispatcher` + // event up through these runes-mode components silently drops it. + onRestore?: (restoredApp: any) => void } let { @@ -137,7 +141,8 @@ onResetToDeployed, loadedFromDraft = false, othersDraftsCount = 0, - onOpenOthersDrafts + onOpenOthersDrafts, + onRestore }: Props = $props() /** Mirror of the path the user is editing in the pen popover. Initialized @@ -862,7 +867,7 @@ (historyBrowserDrawerOpen = false)}> - + onRestore?.(e.detail)} appPath={$appPath} /> diff --git a/frontend/src/lib/components/apps/types.ts b/frontend/src/lib/components/apps/types.ts index de34f4676d..d8ac000ba5 100644 --- a/frontend/src/lib/components/apps/types.ts +++ b/frontend/src/lib/components/apps/types.ts @@ -176,6 +176,10 @@ export interface AppEditorProps { loadedFromDraft?: boolean othersDraftsCount?: number onOpenOthersDrafts?: () => void + // Restoring an older deployment from the history drawer. Threaded through + // AppEditorHeader as a callback prop rather than `on:restore` forwarding, + // which does not propagate through these runes-mode components. + onRestore?: (restoredApp: any) => void } export type App = { diff --git a/frontend/src/lib/components/raw_apps/RawAppEditor.svelte b/frontend/src/lib/components/raw_apps/RawAppEditor.svelte index c8d15e6e7e..18f9c1af0c 100644 --- a/frontend/src/lib/components/raw_apps/RawAppEditor.svelte +++ b/frontend/src/lib/components/raw_apps/RawAppEditor.svelte @@ -121,6 +121,14 @@ onOpenOthersDrafts?: () => void onRuntimeLogRequester?: (requester: RawAppRuntimeLogRequester | undefined) => void onRunsProvider?: (provider: RawAppRunsProvider | undefined) => void + // Restoring an older deployment from the history drawer. A callback prop + // (not `on:restore` forwarding): forwarding a `createEventDispatcher` + // event up through these runes-mode components silently drops it. + onRestore?: (restoredApp: any) => void + // Deploy created the app at a new path; the page navigates to it. Callback + // prop for the same reason as `onRestore` — `on:savedNewAppPath` forwarding + // through these runes-mode components is dropped. + onSavedNewAppPath?: (path: string) => void } let { @@ -148,7 +156,9 @@ othersDraftsCount = 0, onOpenOthersDrafts, onRuntimeLogRequester = undefined, - onRunsProvider = undefined + onRunsProvider = undefined, + onRestore, + onSavedNewAppPath }: Props = $props() export const version: number | undefined = undefined @@ -1596,8 +1606,8 @@ bind:savedApp bind:summary bind:pendingDraftPath - on:restore - on:savedNewAppPath + {onRestore} + {onSavedNewAppPath} {policy} {diffDrawer} {newApp} diff --git a/frontend/src/lib/components/raw_apps/RawAppEditorHeader.svelte b/frontend/src/lib/components/raw_apps/RawAppEditorHeader.svelte index 3963e1b16d..9853fb69bd 100644 --- a/frontend/src/lib/components/raw_apps/RawAppEditorHeader.svelte +++ b/frontend/src/lib/components/raw_apps/RawAppEditorHeader.svelte @@ -26,7 +26,7 @@ Undo, WandSparkles } from 'lucide-svelte' - import { createEventDispatcher, untrack } from 'svelte' + import { untrack } from 'svelte' import { orderedJsonStringify, type Value, replaceFalseWithUndefined } from '../../utils' import { random_adj } from '$lib/components/random_positive_adjetive' @@ -151,6 +151,14 @@ loadedFromDraft?: boolean othersDraftsCount?: number onOpenOthersDrafts?: () => void + // Restoring an older deployment from the history drawer. A callback prop + // (not `on:restore` forwarding): forwarding a `createEventDispatcher` + // event up through these runes-mode components silently drops it. + onRestore?: (restoredApp: any) => void + // Deploy created the app at a new path; the page navigates to it. Callback + // prop for the same reason as `onRestore` — `on:savedNewAppPath` forwarding + // through these runes-mode components is dropped. + onSavedNewAppPath?: (path: string) => void } let { @@ -184,7 +192,9 @@ onResetToDeployed, loadedFromDraft = false, othersDraftsCount = 0, - onOpenOthersDrafts + onOpenOthersDrafts, + onRestore, + onSavedNewAppPath }: Props = $props() // Set by the on-behalf-of selector when the publisher picks a user other than @@ -352,7 +362,7 @@ path: appPath }) } - dispatch('savedNewAppPath', path) + onSavedNewAppPath?.(path) onDeploy?.({ path }) } catch (e) { sendUserToast(`Error creating app: ${e.body ?? e.message}`, true) @@ -475,7 +485,7 @@ }) } if (appPath !== npath) { - dispatch('savedNewAppPath', npath) + onSavedNewAppPath?.(npath) } onDeploy?.({ path: npath }) } @@ -575,8 +585,6 @@ } ]) - const dispatch = createEventDispatcher() - let customPath = $state(savedApp?.custom_path) let customPathError = $state('') @@ -688,7 +696,7 @@ (historyBrowserDrawerOpen = false)}> - + onRestore?.(e.detail)} {appPath} /> diff --git a/frontend/src/routes/(root)/(logged)/apps/edit/[...path]/+page.svelte b/frontend/src/routes/(root)/(logged)/apps/edit/[...path]/+page.svelte index 8c0efa0af4..ea87409171 100644 --- a/frontend/src/routes/(root)/(logged)/apps/edit/[...path]/+page.svelte +++ b/frontend/src/routes/(root)/(logged)/apps/edit/[...path]/+page.svelte @@ -388,9 +388,14 @@ let diffDrawer: DiffDrawer | undefined = $state() - function onRestore(ev: any) { + function onRestore(restoredApp: any) { sendUserToast('App restored from previous deployment') - app = ev.detail + // Drop the stale pre-restore autosave. The remounted AppEditor seeds its + // state from `appDraftHandle.draft ?? app`, so without this it keeps showing + // the old draft instead of the restored version. Same reason `reloadDeployed` + // removes the draft before remounting. + UserDraft.remove('app', path) + app = restoredApp // Re-pin the stale-draft fork base to the current head. A restored value // carries the `parent_version` baked in when that older version was deployed, // which would make the deploy guard (`compareVersions`) falsely report "not on @@ -470,7 +475,7 @@ app.path = url } }} - on:restore={onRestore} + {onRestore} summary={app.summary} app={app.value} {deployedBaseline} diff --git a/frontend/src/routes/(root)/(logged)/apps_raw/edit/[...path]/+page.svelte b/frontend/src/routes/(root)/(logged)/apps_raw/edit/[...path]/+page.svelte index 94c6d72e71..0cf374f0b1 100644 --- a/frontend/src/routes/(root)/(logged)/apps_raw/edit/[...path]/+page.svelte +++ b/frontend/src/routes/(root)/(logged)/apps_raw/edit/[...path]/+page.svelte @@ -449,9 +449,9 @@ let diffDrawer: DiffDrawer | undefined = $state(undefined) - function onRestore(ev: any) { + function onRestore(restoredApp: any) { sendUserToast('App restored from previous deployment') - let prev = ev.detail + let prev = restoredApp extractRawApp(prev) savedApp = { summary: prev.summary, @@ -536,12 +536,12 @@ {#key redraw}
{ + onSavedNewAppPath={(savedPath) => { draftSync.remove() - goto(`/apps_raw/edit/${event.detail}`) - newPath = event.detail + goto(`/apps_raw/edit/${savedPath}`) + newPath = savedPath }} - on:restore={onRestore} + {onRestore} bind:files bind:runnables bind:data