mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-09-13 16:05:00 +00:00
feat(frontend): wire raw app editor to UserDraft
/apps_raw/edit owns the canonical raw-app state (files, runnables,
data, summary) in four $state vars; a single $effect deep-tracks them
and forwards the bundle to a UserDraft.use<RawAppDraft> handle so each
mutation tick persists at userdraft/w/{ws}/raw_app/{path} (deduped by
useLocalStorageValue's serialized check). On load the route overlays
the local autosave on top of backend.draft/deployed and offers a
"Discard / Show diff" toast when they diverge; matching local entries
are silently dropped. Deploy, save-as-draft rename, restore-draft and
restore-deployed each call UserDraft.remove on the route path.
/apps_raw/add keeps the same shape (UserDraft.use with empty path)
so the draft is in-memory only and we drop it explicitly when the
initial save creates the real path.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.7
parent
1e77e46624
commit
b7ca257239
@@ -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)
|
||||
|
||||
@@ -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<string, string>
|
||||
runnables: Record<string, Runnable>
|
||||
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) {
|
||||
|
||||
@@ -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<string, string>
|
||||
runnables: Record<string, any>
|
||||
data: RawAppData
|
||||
summary: string
|
||||
}
|
||||
|
||||
let files: Record<string, string> | 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<RawAppDraft>('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<RawAppDraft>('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 @@
|
||||
<div class="h-screen">
|
||||
<RawAppEditor
|
||||
on:savedNewAppPath={(event) => {
|
||||
UserDraft.remove('raw_app', path)
|
||||
goto(`/apps_raw/edit/${event.detail}`)
|
||||
newPath = event.detail
|
||||
}}
|
||||
|
||||
Reference in New Issue
Block a user