diff --git a/frontend/src/lib/components/raw_apps/RawAppEditorHeader.svelte b/frontend/src/lib/components/raw_apps/RawAppEditorHeader.svelte index 7c063de9bd..b0a441d6d9 100644 --- a/frontend/src/lib/components/raw_apps/RawAppEditorHeader.svelte +++ b/frontend/src/lib/components/raw_apps/RawAppEditorHeader.svelte @@ -4,6 +4,7 @@ import UndoRedo from '$lib/components/common/button/UndoRedo.svelte' import { AppService, DraftService, type Policy } from '$lib/gen' + import { UserDraft } from '$lib/userDraft.svelte' import { rawAppToHubUrl } from '$lib/hub' import { enterpriseLicense, hubBaseUrlStore, userStore, workspaceStore } from '$lib/stores' import YAML from 'yaml' @@ -233,6 +234,7 @@ } closeSaveDrawer() sendUserToast('App deployed successfully') + UserDraft.remove('raw_app', path) dispatch('savedNewAppPath', path) } catch (e) { sendUserToast('Error creating app', e) @@ -342,6 +344,7 @@ closeSaveDrawer() sendUserToast('App deployed successfully') + UserDraft.remove('raw_app', appPath) if (appPath !== npath) { dispatch('savedNewAppPath', npath) } @@ -420,6 +423,9 @@ } draftDrawerOpen = false + // /apps_raw/add was in-memory (empty path); drop that entry so it + // doesn't shadow a future fresh /add visit before we navigate. + UserDraft.remove('raw_app', appPath) dispatch('savedNewAppPath', newEditedPath) } catch (e) { sendUserToast('Error saving initial draft', e) @@ -520,6 +526,7 @@ } sendUserToast('Draft saved') + UserDraft.remove('raw_app', path) loading.saveDraft = false if (newApp || savedApp.draft_only) { dispatch('savedNewAppPath', newEditedPath || path) diff --git a/frontend/src/routes/(root)/(logged)/apps_raw/add/+page.svelte b/frontend/src/routes/(root)/(logged)/apps_raw/add/+page.svelte index fac99b3fba..12eae5c2eb 100644 --- a/frontend/src/routes/(root)/(logged)/apps_raw/add/+page.svelte +++ b/frontend/src/routes/(root)/(logged)/apps_raw/add/+page.svelte @@ -11,6 +11,8 @@ import RawAppEditor from '$lib/components/raw_apps/RawAppEditor.svelte' import Modal from '$lib/components/common/modal/Modal.svelte' import FileEditorIcon from '$lib/components/raw_apps/FileEditorIcon.svelte' + import { UserDraft } from '$lib/userDraft.svelte' + import { readFieldsRecursively } from '$lib/utils' import { react18Template, react19Template, svelte5Template } from './templates' import type { Runnable } from '$lib/components/raw_apps/rawAppPolicy' import { type RawAppData, DEFAULT_DATA } from '$lib/components/raw_apps/dataTableRefUtils' @@ -97,6 +99,23 @@ }) /** Data configuration including tables and creation policy */ let data: RawAppData = $state({ ...DEFAULT_DATA }) + + // Empty path → in-memory only; this just keeps the API uniform with + // /apps_raw/edit so multiple components reading the draft stay in sync. + const draftHandle = UserDraft.use<{ + files: Record + runnables: Record + data: RawAppData + summary: string + }>('raw_app', '') + $effect(() => { + readFieldsRecursively(files) + readFieldsRecursively(runnables) + readFieldsRecursively(data) + void summary + draftHandle.draft = { files, runnables, data, summary } + }) + loadApp() function extractValue(value: any) { 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 a1219f2e4e..70a2acfaba 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 @@ -3,7 +3,12 @@ import { AppService, DraftService } from '$lib/gen' import { workspaceStore } from '$lib/stores' - import { cleanValueProperties, type Value } from '$lib/utils' + import { + cleanValueProperties, + orderedJsonStringify, + readFieldsRecursively, + type Value + } from '$lib/utils' import { afterNavigate, replaceState } from '$app/navigation' import { goto } from '$lib/navigation' import { sendUserToast } from '$lib/toast' @@ -13,6 +18,15 @@ import { stateSnapshot } from '$lib/svelte5Utils.svelte' import { page } from '$app/state' import { type RawAppData, DEFAULT_DATA } from '$lib/components/raw_apps/dataTableRefUtils' + import { UserDraft } from '$lib/userDraft.svelte' + + type RawAppDraft = { + files: Record + runnables: Record + data: RawAppData + summary: string + } + let files: Record | undefined = $state(undefined) let runnables = $state({}) /** Data configuration including tables and creation policy */ @@ -39,6 +53,18 @@ let redraw = $state(0) let path = page.params.path ?? '' + const draftHandle = UserDraft.use('raw_app', path) + + // Persist the bundle whenever any of the four pieces of state changes. + $effect(() => { + if (!files) return + readFieldsRecursively(files) + readFieldsRecursively(runnables) + readFieldsRecursively(data) + void summary + draftHandle.draft = { files, runnables, data, summary } + }) + let nodraft = page.url.searchParams.get('nodraft') afterNavigate(() => { @@ -90,10 +116,61 @@ custom_path: app_w_draft_.custom_path } - if (app_w_draft.draft) { - extractRawApp(app_w_draft.draft) + const backendSource: any = app_w_draft.draft ? app_w_draft.draft : app_w_draft + const localDraft = UserDraft.get('raw_app', path) + const backendBundle: RawAppDraft = { + files: backendSource.value?.files ?? {}, + runnables: backendSource.value?.runnables ?? {}, + data: + backendSource.value?.data ?? + (backendSource.value?.datatables + ? { ...DEFAULT_DATA, tables: backendSource.value.datatables } + : { ...DEFAULT_DATA }), + summary: backendSource.summary ?? '' + } - if (!app_w_draft.draft_only) { + if ( + localDraft != undefined && + orderedJsonStringify(cleanValueProperties(localDraft)) !== + orderedJsonStringify(cleanValueProperties(backendBundle)) + ) { + const reloadAction = async () => { + UserDraft.remove('raw_app', path) + await loadApp() + redraw++ + } + const deployed = cleanValueProperties(app_w_draft as Value) + const local = { ...deployed, value: localDraft } + sendUserToast('App restored from local autosave', false, [ + { + label: 'Discard local autosave and reload', + callback: reloadAction + }, + { + label: 'Show diff', + callback: async () => { + diffDrawer?.openDrawer() + diffDrawer?.setDiff({ + mode: 'simple', + original: deployed, + current: local, + title: `${app_w_draft.draft ? 'Latest saved draft' : 'Deployed'} <> Autosave`, + button: { text: 'Discard autosave', onClick: reloadAction } + }) + } + } + ]) + runnables = localDraft.runnables + data = localDraft.data + summary = localDraft.summary + policy = app_w_draft.policy + newPath = app_w_draft.path + files = localDraft.files + } else { + if (localDraft != undefined) UserDraft.remove('raw_app', path) + extractRawApp(backendSource) + + if (app_w_draft.draft && !app_w_draft.draft_only) { const reloadAction = () => { extractRawApp(app_w_draft) redraw++ @@ -121,8 +198,6 @@ } ]) } - } else { - extractRawApp(app_w_draft) } } @@ -138,6 +213,7 @@ return } diffDrawer?.closeDrawer() + UserDraft.remove('raw_app', path) goto(`/apps/edit/${savedApp.draft.path}`) await loadApp() redraw++ @@ -156,6 +232,7 @@ path: savedApp.path }) } + UserDraft.remove('raw_app', path) goto(`/apps/edit/${savedApp.path}`) await loadApp() redraw++ @@ -185,6 +262,7 @@
{ + UserDraft.remove('raw_app', path) goto(`/apps_raw/edit/${event.detail}`) newPath = event.detail }}