Files
windmill/frontend/src/lib/newDraftFlag.ts
T
GuilhemandClaude Opus 4.8 9705d60284 fix(frontend): keep session-exit URL clean by syncing new_draft strip with the router (#10101)
* fix(frontend): keep session-exit URL clean by syncing new_draft strip with the router

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* chore(frontend): correct replaceState comment and test-mock wording per review

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* docs(frontend): correct replaceState comment and drop drafting-history phrasing

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-15 17:56:19 +02:00

78 lines
3.4 KiB
TypeScript

import { page } from '$app/state'
import { replaceState } from '$app/navigation'
import { rememberNavRoute } from '$lib/components/sessions/sessionSwitch.svelte'
import { UserDraftDbSyncer, type UserDraftLastSyncQuery } from '$lib/userDraftDbSyncer.svelte'
import { getLocalDraftHint } from '$lib/localDraftHints.svelte'
import type { UserDraftItemKind } from '$lib/gen'
/** Drop `?new_draft=true` from the current URL (preserving every other param),
* mutating the address bar without a navigation. No-op when the flag is absent
* or `window` is unavailable (SSR).
*
* Uses SvelteKit's `replaceState` (not raw `history.replaceState`, which the
* router warns conflicts with it) so the history entry keeps the router's
* bookkeeping and `page.state`. Also refreshes the remembered nav route:
* `afterNavigate` never observes this in-place rewrite, so without it
* `exitSessionMode` would restore the pre-strip URL — still carrying
* `?new_draft=true` — and re-enter the seed-empty branch. */
export function stripNewDraftFlag(): void {
if (typeof window === 'undefined') return
const url = new URL(window.location.href)
if (url.searchParams.get('new_draft') !== 'true') return
url.searchParams.delete('new_draft')
replaceState(url, page.state)
rememberNavRoute(url.pathname + url.search)
}
/**
* Whether an editor load should take the `?new_draft` seed-empty branch.
*
* The `/{scripts,flows,apps,apps_raw}/add` redirect lands the editor on
* `u/<user>/draft_<uuid>?new_draft=true`; the seed branch bootstraps an empty
* (or template/hub/fork-seeded) editor instead of fetching a row that doesn't
* exist yet. But the flag can outlive the seed: exiting an AI session restores
* the remembered nav route (still `?new_draft=true`) even though the draft was
* already materialized — by the first autosave, or by "Open in AI session"'s
* `forcePersist`. Re-running the seed branch then blanks the saved draft.
*
* So gate the seed branch on `new_draft=true` AND no persisted draft this
* session (`getLocalDraftHint`): a never-saved draft still seeds empty (and a
* pre-edit refresh re-seeds instead of 404-ing), while a re-entry after the
* draft exists falls through to the normal load that reflects it.
*/
export function shouldSeedNewDraft(
searchParams: URLSearchParams,
workspace: string | undefined,
itemKind: UserDraftItemKind,
path: string
): boolean {
if (searchParams.get('new_draft') !== 'true') return false
return getLocalDraftHint(workspace, itemKind, path) !== true
}
/**
* Defer stripping `?new_draft=true` from the URL until the draft's first save
* is CONFIRMED by the backend. The `/{scripts,flows,apps,apps_raw}/add`
* redirect lands the editor on an unsaved `u/<user>/draft_<uuid>` path;
* stripping the flag on load (the old behavior) meant a refresh before any
* edit fell through to the `getByPath` fetch for a row that doesn't exist yet
* → 404 "not found". Keeping the flag until `onSaved` fires lets a pre-edit
* refresh re-enter the new-draft seeding branch, while a post-save refresh
* loads the now-persisted draft.
*
* Returns a cleanup to drop the listener on navigation / unmount; the listener
* also self-unsubscribes after the first save.
*/
export function stripNewDraftFlagOnSave(query: UserDraftLastSyncQuery): () => void {
let unsub: (() => void) | undefined
unsub = UserDraftDbSyncer.onSaved(query, () => {
unsub?.()
unsub = undefined
stripNewDraftFlag()
})
return () => {
unsub?.()
unsub = undefined
}
}