Files
windmill/frontend/src/lib/components/FlowWrapper.svelte
T
Diego ImbertandClaude Opus 4.8 25a891041d wire DB-backed autosave into the whitelabel flow SDK (#9637)
* 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>
2026-06-17 17:22:42 +00:00

94 lines
3.2 KiB
Svelte

<script lang="ts">
import { untrack } from 'svelte'
import AiChatLayout from './copilot/chat/AiChatLayout.svelte'
import type { FlowBuilderProps } from './flow_builder'
import FlowBuilder from './FlowBuilder.svelte'
import { usePageDraftSync } from './usePageDraftSync.svelte'
import { workspaceStore } from '$lib/stores'
import type { OpenFlow } from '$lib/gen'
let {
flowStore: oldFlowStore,
flowStateStore: oldFlowStateStore,
disableAi,
light,
...props
}: FlowBuilderProps & { light?: boolean } = $props()
let flowStateStore = $state(untrack(() => oldFlowStateStore))
let trialRender = $state(true)
if (untrack(() => light)) {
setTimeout(() => {
trialRender = false
}, 1000 * 300)
}
// Stable per-user draft storage key. Captured once so editing the flow's path
// (which lives in `draft_path`, not the storage key) can't re-key the autosave
// handle and orphan the draft. Mirrors the full-page editor keying on the URL
// path; falls back through the SDK's path inputs.
const draftStoragePath = untrack(
() =>
props.initialPath ||
props.pathStoreInit ||
(oldFlowStore.val as { path?: string } | undefined)?.path ||
''
)
// Reuse the full-page flow editor's draft orchestration so the SDK gets
// autosave + the AutosaveIndicator (gated by FlowBuilder on
// `liveEditorDraftStoragePath`) — and `recordRemoteSync`/`seedBaseline`/
// `discardIf` if it ever loads a server draft — from one code path. The SDK
// may mount before login, so `useReactive` hands out a detached local-only
// handle until `$workspaceStore` resolves (no throw). The builder itself is
// gated on the workspace below so no edits are made into that detached handle
// — they'd be lost when it re-keys to the real entry on login.
// `defaultValue` seeds the handle from the consumer's loaded flow on first
// acquire (swallowed by the seed guard, never POSTs) — captured once so it
// doesn't churn the reconcile.
const initialFlow = untrack(() => oldFlowStore.val)
const draftSync = usePageDraftSync<OpenFlow>({
itemKind: 'flow',
path: () => draftStoragePath,
workspace: () => $workspaceStore,
defaultValue: initialFlow
})
// Bound store the builder reads/writes, backed by the draft handle. Falls back
// to the consumer-provided value in the first-render window before the handle
// is acquired.
const flowStore = {
get val() {
return draftSync.draft ?? oldFlowStore.val
},
set val(v: OpenFlow) {
draftSync.draft = v
}
}
</script>
{#if trialRender}
<AiChatLayout noPadding={true} {disableAi}>
{#if light}<div class="bg-red-500 absolute z-10">Trial version</div>{/if}
<!-- Gate on a resolved workspace: the draft handle is detached (local-only)
until one exists, so editing before then would be lost on the re-key. -->
{#if $workspaceStore}
<FlowBuilder
{flowStore}
{flowStateStore}
{disableAi}
{...props}
liveEditorDraftStoragePath={draftStoragePath || undefined}
/>
{/if}
</AiChatLayout>
{:else}
<div class="flex flex-col items-center justify-center h-screen">
<div class="text-2xl font-bold"
>Windmill Whitelabel SDK is in trial mode and disabled itself after 5 minutes</div
>
</div>
{/if}