mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-08-23 08:00:45 +00:00
feat: add UserDraft.stopSync/restartSync, wire on script + low-code app /add init
This commit is contained in:
@@ -100,6 +100,7 @@
|
||||
import { buildForkEditUrl } from '$lib/utils/editInFork'
|
||||
import OnBehalfOfSelector, { type OnBehalfOfChoice } from './OnBehalfOfSelector.svelte'
|
||||
import WacExportDrawer from './scripts/WacExportDrawer.svelte'
|
||||
import { UserDraft } from '$lib/userDraft.svelte'
|
||||
|
||||
let {
|
||||
script = $bindable(),
|
||||
@@ -343,6 +344,14 @@
|
||||
let loadingSave = $state(false)
|
||||
|
||||
if (script.content == '') {
|
||||
// Suspend autosave around the bootstrap mutations — seeding the
|
||||
// editor with the template's `initialCode` is a programmatic
|
||||
// write that shouldn't count as the user's "first edit" and
|
||||
// shouldn't POST to the server. The route's UserDraft handle is
|
||||
// keyed by `initialPath` (the URL path). Resumed in the async
|
||||
// `.finally` so language switches AFTER bootstrap (which also
|
||||
// call `initContent`) sync normally.
|
||||
UserDraft.stopSync('script', initialPath)
|
||||
if (template === 'wac_python') {
|
||||
script.modules = {
|
||||
'helper.py': {
|
||||
@@ -358,7 +367,9 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
initContent(script.language, script.kind, template)
|
||||
initContent(script.language, script.kind, template).finally(() => {
|
||||
UserDraft.restartSync('script', initialPath)
|
||||
})
|
||||
}
|
||||
|
||||
async function isTemplateScript() {
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
const bubble = createBubbler()
|
||||
import SplitPanesWrapper from '$lib/components/splitPanes/SplitPanesWrapper.svelte'
|
||||
import { getContext, onMount, setContext, untrack } from 'svelte'
|
||||
import { getContext, onMount, setContext, tick, untrack } from 'svelte'
|
||||
import { twMerge } from 'tailwind-merge'
|
||||
|
||||
import { Pane, Splitpanes } from 'svelte-splitpanes'
|
||||
@@ -94,6 +94,15 @@
|
||||
|
||||
const appDraftPath = newApp ? '' : (path ?? '')
|
||||
const appDraftHandle = inSessionPane ? undefined : UserDraft.use<App>('app', appDraftPath)
|
||||
// Suspend autosave around mount — the route may have seeded `app`
|
||||
// from an empty template (e.g. `/apps/add` redirect with
|
||||
// `new_draft=true`), and the `firstMirror` effect below writes that
|
||||
// seed into the handle as a programmatic mutation. Without
|
||||
// suspension that write fires a POST that looks like the user's
|
||||
// first edit before they've touched anything. `onMount`-then-`tick`
|
||||
// resumes once all mount-time effects have settled, so the user's
|
||||
// real first edit is the first POST.
|
||||
if (appDraftHandle) UserDraft.stopSync('app', appDraftPath)
|
||||
// Prefer the persisted autosave over the prop when both exist (e.g.
|
||||
// /apps/add reload: the route always initializes `app` to an empty
|
||||
// template, but the user's last session is sitting in LS under the
|
||||
@@ -481,6 +490,14 @@
|
||||
let mounted = false
|
||||
onMount(() => {
|
||||
mounted = true
|
||||
// Resume autosave now that mount-time effects (the
|
||||
// `firstMirror` mirror, prop-driven initialization, ...) have
|
||||
// all run. `tick` waits for the current pending effect flush
|
||||
// to complete so the post-suspend writes have been observed by
|
||||
// the sync effect and silently dropped.
|
||||
if (appDraftHandle) {
|
||||
tick().then(() => UserDraft.restartSync('app', appDraftPath))
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
if ($initialized?.initialized === false) {
|
||||
|
||||
@@ -117,6 +117,15 @@ type DraftEntry = {
|
||||
* incorrect POST.
|
||||
*/
|
||||
skipNextSync: boolean
|
||||
/**
|
||||
* Sticky version of `skipNextSync`. While true, the reactive sync
|
||||
* effect updates the local state but never POSTs — used by callers
|
||||
* that programmatically mutate the draft as part of bootstrapping
|
||||
* (e.g. setting the editor's `initialCode` after mount) and don't
|
||||
* want those writes to land on the server as the user's "first
|
||||
* autosave". Toggled via `UserDraft.stopSync` / `restartSync`.
|
||||
*/
|
||||
syncSuspended: boolean
|
||||
/**
|
||||
* Tears down the `$effect.root` scope that owns the entry's sync
|
||||
* effect. Called when the refcount hits 0. `undefined` only when the
|
||||
@@ -410,6 +419,38 @@ export const UserDraft = {
|
||||
UserDraft.discard(itemKind, path, undefined, opts)
|
||||
},
|
||||
|
||||
/**
|
||||
* Suspend the reactive sync for `(workspace, itemKind, path)`.
|
||||
* Writes after this call still update the in-memory cell and any
|
||||
* subscribers but don't POST to the syncer. Use to bracket
|
||||
* programmatic mutations that happen during editor bootstrap (e.g.
|
||||
* seeding script content from `initialCode`, low-code app init)
|
||||
* so they don't appear on the server as the user's "first edit".
|
||||
*
|
||||
* The entry must already be live (acquired by `use`/`useMany`); a
|
||||
* no-op otherwise. Pair every `stopSync` with a `restartSync` —
|
||||
* forgetting to resume silently turns off autosave for the rest of
|
||||
* the session.
|
||||
*/
|
||||
stopSync(itemKind: UserDraftItemKind, path: string, opts?: UserDraftOptions): void {
|
||||
const ws = resolveWorkspace(opts)
|
||||
const entry = entries.get(mapKey(ws, itemKind, path))
|
||||
if (entry) entry.syncSuspended = true
|
||||
},
|
||||
|
||||
/**
|
||||
* Resume reactive sync for `(workspace, itemKind, path)` after a
|
||||
* `stopSync`. Subsequent writes that differ from the suspended-time
|
||||
* state are POSTed normally; writes made during the suspension are
|
||||
* dropped from the server's view (the local cell still reflects
|
||||
* them). No-op if the entry isn't live or wasn't suspended.
|
||||
*/
|
||||
restartSync(itemKind: UserDraftItemKind, path: string, opts?: UserDraftOptions): void {
|
||||
const ws = resolveWorkspace(opts)
|
||||
const entry = entries.get(mapKey(ws, itemKind, path))
|
||||
if (entry) entry.syncSuspended = false
|
||||
},
|
||||
|
||||
/**
|
||||
* List currently-mounted live entries for `workspace`. Without the
|
||||
* localStorage layer, "list" is meaningful only for in-tab entries —
|
||||
@@ -649,6 +690,11 @@ function acquireEntry(
|
||||
entry.skipNextSync = false
|
||||
return
|
||||
}
|
||||
// `syncSuspended` swallows the POST but still advances
|
||||
// `lastSerialized` (above) so when sync resumes the next
|
||||
// real change is detected as a change — only the writes
|
||||
// made during suspension are dropped from the server's view.
|
||||
if (entry?.syncSuspended) return
|
||||
void UserDraftDbSyncer.save({
|
||||
workspace,
|
||||
itemKind,
|
||||
@@ -665,6 +711,7 @@ function acquireEntry(
|
||||
path,
|
||||
state: stateRef,
|
||||
skipNextSync: false,
|
||||
syncSuspended: false,
|
||||
destroyRoot
|
||||
})
|
||||
return
|
||||
@@ -680,7 +727,8 @@ function acquireEntry(
|
||||
itemKind,
|
||||
path,
|
||||
state: fallback,
|
||||
skipNextSync: false
|
||||
skipNextSync: false,
|
||||
syncSuspended: false
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user