From 0be6bc14264ff292be508e9e6995a972dde691a4 Mon Sep 17 00:00:00 2001 From: Diego Imbert Date: Tue, 9 Jun 2026 12:53:51 +0200 Subject: [PATCH] =?UTF-8?q?fix(drafts):=20low-code=20apps=20=E2=80=94=20dr?= =?UTF-8?q?op=20spurious=20autosave=20on=20/edit=20+=20remount=20on=20Load?= =?UTF-8?q?=20from=20server?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two bugs in low-code app editor (raw apps use a separate code path): 1. Every /edit visit looked like an autosave because loadApp() called UserDraft.discard('app', path, undefined). The comment claimed "this load doesn't POST" but discard always POSTs value: null server-side — that surfaced as a DELETE-my-draft on every page load AND a flash in the AutosaveIndicator. The discard was originally intended to wipe the in-memory cell so AppEditor remounts "fresh". But the path-change $effect upstream already sets app = undefined before each loadApp, which unmounts AppEditor and releases the handle's entry — so a remount via app = backendApp naturally starts with an empty handle. Drop the discard. 2. The conflict modal's "Load from server" called loadApp() but didn't remount AppEditor. Since AppEditor's stateApp is captured once at mount and doesn't react to prop changes, the editor kept showing the conflicting local edits even after a successful reload. Wrap the onLoadFromServer to await loadApp() then bump redraw to force a fresh mount. --- .../(logged)/apps/edit/[...path]/+page.svelte | 22 ++++++++++++++----- 1 file changed, 16 insertions(+), 6 deletions(-) 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 ef8e5a56e0..e80f1e064c 100644 --- a/frontend/src/routes/(root)/(logged)/apps/edit/[...path]/+page.svelte +++ b/frontend/src/routes/(root)/(logged)/apps/edit/[...path]/+page.svelte @@ -166,11 +166,14 @@ policy: backendApp_.policy, custom_path: backendApp_.custom_path } - // Backend canonical: wipe the in-memory cell so AppEditor remounts - // fresh from `backendApp.value`. The cell will be re-seeded by - // AppEditor's mirror $effect; the first such write is swallowed - // by `acquireEntry`'s seed guard so this load doesn't POST. - UserDraft.discard('app', path, undefined) + // Backend canonical: assign the fresh response onto `app`. The + // path-change $effect upstream sets `app = undefined` before + // calling loadApp, which unmounts AppEditor and releases the + // UserDraft entry — so when `app = backendApp` triggers a + // remount, the new handle starts fresh with no stale draft to + // dedup against. (`UserDraft.discard` was here previously but + // it POSTs `value: null` server-side, which surfaced as a + // spurious autosave on every /edit visit.) app = backendApp } @@ -225,7 +228,14 @@ {path} {otherDraftsUsers} editPathFor={(forkedPath) => `/apps/edit/${forkedPath}`} - onLoadFromServer={() => loadApp()} + onLoadFromServer={async () => { + // AppEditor's `stateApp` is captured once at mount and doesn't + // react to prop changes, so a plain loadApp() reload would update + // `app` but leave the editor displaying the conflicting local + // state. `redraw++` remounts AppEditor against the fresh `app`. + await loadApp() + redraw++ + }} getLocalDraft={() => app?.value} />