mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-10-28 08:02:43 +00:00
* fix(frontend): wire DB-backed autosave into the whitelabel flow SDK FlowWrapper (the @windmill-labs/components flow editor entry) was never updated after DB-backed user drafts moved autosave wiring to the page layer, so the SDK editor had no autosave and never rendered the AutosaveIndicator. Back the bound store with a per-user UserDraft handle (workspace-guarded so it no-ops before a workspace exists) and pass liveEditorDraftStoragePath so the indicator and Ctrl/Cmd+S flush engage. Also set $workspaceStore on the /test_dev/sdk_flow harness page, which lives outside the (logged) layout and so had an empty workspace store (mirrors the sibling sdk_resource page). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(frontend): shared test_dev header to log in + set the SDK token Add a common TestDevHeader (rendered by a test_dev/+layout) that logs in (email/password → bearer token), lets a token be pasted/set manually, picks the workspace, loads the user, and persists the session across reloads — mirroring the React SDK's initializeClients. test_dev routes live outside the (logged) layout, so this is the single place that wires OpenAPI.TOKEN + workspaceStore + userStore for the SDK demo pages. Drop the now-redundant per-page workspace/user wiring from sdk_flow. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(frontend): reuse usePageDraftSync in the flow SDK instead of a parallel copy FlowWrapper hand-rolled UserDraft.useMany + a manual seed effect, duplicating the core of usePageDraftSync but dropping recordRemoteSync/seedBaseline/discardIf — a divergence that would drift. The only reason it couldn't reuse the helper was that useReactive passes workspace straight into useMany, whose reconcile called resolveWorkspace() (which throws) before the detached-handle check. Make reconcile resolve the workspace without throwing and treat an absent workspace like an empty path — handing out a detached, local-only handle that re-keys into a real entry once the workspace resolves. FlowWrapper then reuses usePageDraftSync directly, keeping one code path for the page and SDK editors. Seed via the spec's defaultValue (threaded through usePageDraftSync -> useReactive -> useMany, captured once on first acquire and swallowed by the syncer's seed guard) rather than a manual first-write effect, dropping the fragile skipNextWrite assumption and the seededPath bookkeeping. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(frontend): wire DB-backed autosave into the whitelabel script SDK ScriptWrapper had the same gap FlowWrapper did: ScriptBuilder delegates its draft handle to the page (it only stop/restart-syncs and flushes by userDraftPath), so the SDK's plain `bind:script` never reached a UserDraft handle — no autosave, no indicator. Back it with usePageDraftSync<script> (bind:script={draftSync.draft}, userDraftPath), seeded from the consumer's script via defaultValue. Same one-code-path reuse as the flow SDK. AppWrapper needs no change: AppEditor already self-acquires its handle (UserDraft.use('app', ...)), so apps autosave already — and now also tolerate mounting before login via the reconcile change. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(frontend): gate SDK editors on a resolved workspace Before a workspace exists the draft handle is detached (local-only); editing into it and then having the workspace resolve re-keys to a fresh real entry seeded from the original value, silently dropping those edits. Gate the flow, script, and app SDK editors on `$workspaceStore` so no editing happens until the real draft key exists. Embedders set the workspace before rendering (React SDK initializeClients); the test_dev header sets it on mount. AppEditor additionally acquires its handle at init from a non-reactive workspace, so gating AppWrapper also ensures it mounts with the workspace already set rather than permanently detached. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(frontend): add sdk_app test_dev page for the app editor SDK Exercises AppWrapper the same way sdk_flow/sdk_script exercise their editors, under the shared TestDevHeader. Confirms the app editor's self-managed autosave + AutosaveIndicator work via the SDK wrapper. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
121 lines
4.5 KiB
TypeScript
121 lines
4.5 KiB
TypeScript
import { untrack } from 'svelte'
|
|
import { UserDraft, type UserDraftHandle, type UserDraftItemKind } from '$lib/userDraft.svelte'
|
|
import { UserDraftDbSyncer } from '$lib/userDraftDbSyncer.svelte'
|
|
|
|
/**
|
|
* Page-level draft orchestration for the full-page editors (scripts / flows /
|
|
* apps / apps_raw) — the page analogue of `useTriggerDraftSync`. Owns the
|
|
* per-path autosave handle (re-keyed on navigation), the live-editor-draft
|
|
* registry entry (home-page "edit draft" links), `recordRemoteSync` (a method
|
|
* so pages can't forget it and silently downgrade conflict detection to
|
|
* overwrite-as-fresh), the deployed-baseline `seed`, and the draft `remove`.
|
|
* The entity-specific backend load and new-draft template stay in the page.
|
|
*/
|
|
export interface PageDraftSyncOptions<V = unknown> {
|
|
itemKind: UserDraftItemKind
|
|
/** Reactive draft storage path. `''` (e.g. viewing a historical hash)
|
|
* releases the handle and skips registry/sync work. */
|
|
path: () => string
|
|
/** Reactive workspace (`$workspaceStore`). */
|
|
workspace: () => string | undefined
|
|
/** Reactive effective path for the live-editor-draft registry (the
|
|
* draft's own `path`, used by home-page deep links). Omit to skip
|
|
* registry registration entirely (e.g. read-only hash views). */
|
|
effectivePath?: () => string | undefined
|
|
/** Predicate: is the value about to autosave back at the deployed
|
|
* baseline? When true the syncer POSTs a delete instead of a
|
|
* baseline-equal draft, so editing back to deployed clears the draft
|
|
* instead of leaving a no-op behind. MUST read the deployed baseline
|
|
* reactively (it's captured once per re-keyed acquire) and use
|
|
* `draftValuesEqual` so it can't disagree with the "unsaved changes"
|
|
* banner. Return false for draft-only items (no deployed baseline). */
|
|
discardIf?: (val: V) => boolean
|
|
/** Seeds the cell on first acquire without POSTing (the syncer's seed
|
|
* guard swallows it). Use when the value is already in hand at mount (an
|
|
* embedder providing the item) instead of assigning `draft` after load.
|
|
* Pass a STABLE reference (read it under `untrack`). */
|
|
defaultValue?: V
|
|
}
|
|
|
|
export interface PageDraftSync<V> {
|
|
/** The live autosave handle. `draft` is the editor's bind target. */
|
|
readonly handle: UserDraftHandle<V>
|
|
get draft(): V | undefined
|
|
set draft(value: V | undefined)
|
|
/** Load `value` as the deployed/template baseline WITHOUT POSTing it
|
|
* (see `UserDraft.seed`). */
|
|
seedBaseline(value: V): void
|
|
/** After a backend load, record the server's `draft_saved_at` so the
|
|
* next autosave attaches a matching `last_sync` and the server can
|
|
* reject stale writes. `undefined` clears it (no draft existed). */
|
|
recordRemoteSync(draftSavedAt: string | undefined): void
|
|
/** Drop the draft (server row + local cell) — restore-to-deployed and
|
|
* post-deploy cleanup. */
|
|
remove(): void
|
|
}
|
|
|
|
export function usePageDraftSync<V = unknown>(opts: PageDraftSyncOptions<V>): PageDraftSync<V> {
|
|
// One handle, re-keyed on (workspace, path); `''` path releases it.
|
|
// `canBeDisabled` because these editors carry the "Enable auto-save" toggle.
|
|
const handle = UserDraft.useReactive<V>(() => ({
|
|
itemKind: opts.itemKind,
|
|
path: opts.path(),
|
|
workspace: opts.workspace(),
|
|
canBeDisabled: true,
|
|
defaultValue: opts.defaultValue,
|
|
discardIf: opts.discardIf
|
|
}))
|
|
|
|
// Live-editor-draft registry: lets the home-page "edit draft" link resolve
|
|
// this open editor. Re-registers on path change; cleared on teardown.
|
|
$effect(() => {
|
|
if (!opts.effectivePath) return
|
|
const ws = opts.workspace()
|
|
const p = opts.path()
|
|
const eff = opts.effectivePath()
|
|
if (!ws || !p) return
|
|
UserDraft.setLiveEditorDraft({
|
|
workspace: ws,
|
|
itemKind: opts.itemKind,
|
|
storagePath: p,
|
|
effectivePath: eff || p
|
|
})
|
|
return () => UserDraft.clearLiveEditorDraft(opts.itemKind, { workspace: ws, storagePath: p })
|
|
})
|
|
|
|
return {
|
|
get handle() {
|
|
return handle
|
|
},
|
|
get draft() {
|
|
return handle.draft
|
|
},
|
|
set draft(value: V | undefined) {
|
|
handle.draft = value
|
|
},
|
|
seedBaseline(value: V) {
|
|
const ws = opts.workspace()
|
|
const p = opts.path()
|
|
if (!ws || !p) return
|
|
UserDraft.seed(opts.itemKind, p, value, { workspace: ws })
|
|
},
|
|
recordRemoteSync(draftSavedAt: string | undefined) {
|
|
const ws = opts.workspace()
|
|
const p = opts.path()
|
|
if (!ws || !p) return
|
|
untrack(() =>
|
|
UserDraftDbSyncer.recordRemoteSync(
|
|
{ workspace: ws, itemKind: opts.itemKind, path: p },
|
|
draftSavedAt
|
|
)
|
|
)
|
|
},
|
|
remove() {
|
|
const ws = opts.workspace()
|
|
const p = opts.path()
|
|
if (!ws || !p) return
|
|
UserDraft.remove(opts.itemKind, p, { workspace: ws })
|
|
}
|
|
}
|
|
}
|