ui(drafts): surface draft state in AutosaveIndicator instead of toast+auto-modal

The "Loaded your saved draft" toast and the auto-opening
OtherUsersDraftsModal both surprised users on every editor mount. Move
both signals into the AutosaveIndicator label: "Loaded from draft" or
"Others are working on this {kind}" (priority) sits where Saving/Saved
do, with a one-shot light-green flash behind the indicator that fades
to transparent. Saving/Saved still win when they fire. The popover
gains a "See others' drafts" button that flips the modal open on
demand; the modal itself is now externally controlled via a bindable
\`isOpen\` threaded through DraftEditorModals.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Diego Imbert
2026-06-09 19:36:30 +02:00
co-authored by Claude Opus 4.7
parent 54c7388071
commit 5e14a7c633
16 changed files with 268 additions and 77 deletions
@@ -1,6 +1,6 @@
<script lang="ts">
import { untrack } from 'svelte'
import { CloudCheck, RefreshCcw, RotateCcw } from 'lucide-svelte'
import { CloudCheck, RefreshCcw, RotateCcw, Users } from 'lucide-svelte'
import type { UserDraftItemKind } from '$lib/gen'
import { UserDraftDbSyncer, type UserDraftSyncState } from '$lib/userDraftDbSyncer.svelte'
import { UserDraft } from '$lib/userDraft.svelte'
@@ -21,13 +21,32 @@
// fires `value: null` at the syncer, awaits this, then restarts
// sync after two ticks — mirrors `notifyDraftLoaded`'s "Reset to
// deployed" toast action so the discard sticks.
onResetToDeployed
onResetToDeployed,
// Set true on the first overlay response that came back with
// `is_draft: true` — triggers the on-mount "Loaded from draft"
// hint label and green-flash animation. Replaces the old
// `notifyDraftLoaded` toast. We snapshot the prop at mount so
// re-renders from prop churn don't re-trigger the hint.
loadedFromDraft = false,
// Number of OTHER workspace users with a draft at this path
// (i.e. the deployed-overlay's `other_drafts_users` length).
// When > 0, the on-mount hint takes priority over "Loaded from
// draft" and the popover offers a "See others' drafts" button
// that flips the OtherUsersDraftsModal open through
// `onOpenOthersDrafts`.
othersDraftsCount = 0,
// Wired by the route to the bindable `othersModalOpen` it
// threads into DraftEditorModals.
onOpenOthersDrafts
}: {
workspace: string
itemKind: UserDraftItemKind
path: string
draftOnly?: boolean
onResetToDeployed?: () => void | Promise<void>
loadedFromDraft?: boolean
othersDraftsCount?: number
onOpenOthersDrafts?: () => void
} = $props()
// `UserDraft.has` reads `entry.state.val` (a $state), so the $derived
@@ -77,8 +96,61 @@
}
})
// On-mount load hint — "Others are working..." or "Loaded from draft".
// Stays for HINT_LABEL_MS, and the wrapper gets a one-shot green-flash
// CSS animation that fades to transparent. Snapshot the props at mount
// so we don't re-fire the hint as the route re-renders the indicator.
const HINT_LABEL_MS = 7000
let hintLabel = $state('')
let hintFlashKey = $state(0)
let hintTimer: ReturnType<typeof setTimeout> | undefined
const kindLabel = $derived(
itemKind === 'flow' ? 'flow' : itemKind === 'app' || itemKind === 'raw_app' ? 'app' : 'script'
)
// Triggers when either prop becomes truthy. Each truthy transition fires
// a fresh flash — wins precedence is computed here too (others > loaded).
// Seed to `false` (NOT to the props' current values) so a prop that is
// already truthy at mount counts as the first false → true transition
// and fires the hint; routes pass `loadedFromDraft = true` immediately
// after the overlay response comes back, so this is the common case.
let prevOthers = false
let prevLoaded = false
$effect(() => {
const othersNow = othersDraftsCount > 0
const loadedNow = !!loadedFromDraft
untrack(() => {
const othersTransition = othersNow && !prevOthers
const loadedTransition = loadedNow && !prevLoaded
if (othersTransition || loadedTransition) {
hintLabel = othersNow ? `Others are working on this ${kindLabel}` : 'Loaded from draft'
hintFlashKey++
if (hintTimer) clearTimeout(hintTimer)
hintTimer = setTimeout(() => {
hintLabel = ''
hintTimer = undefined
}, HINT_LABEL_MS)
}
prevOthers = othersNow
prevLoaded = loadedNow
})
})
$effect(() => {
return () => {
if (hintTimer) clearTimeout(hintTimer)
}
})
// "Saving..." / "Saved" beat any hint; otherwise the hint shows. Empty
// string collapses the label `<span>`.
const label = $derived(
syncState === 'saving' || syncState === 'pending' ? 'Saving...' : savedVisible ? 'Saved' : ''
syncState === 'saving' || syncState === 'pending'
? 'Saving...'
: savedVisible
? 'Saved'
: hintLabel
)
const showResetAction = $derived(!draftOnly && hasDraft && !!onResetToDeployed)
@@ -96,12 +168,28 @@
popoverOpen = false
}
}
function openOthersDrafts() {
popoverOpen = false
onOpenOthersDrafts?.()
}
</script>
<div
class="flex items-center gap-1.5 text-primary min-w-[4.2rem]"
class="autosave-indicator-wrap relative flex items-center gap-1.5 text-primary min-w-[4.2rem] rounded-md"
aria-label="Autosave status"
>
{#if hintLabel}
<!-- One-shot green flash behind the indicator when a load hint appears.
Keyed on `hintFlashKey` so re-triggering the hint replays the
animation (Svelte tears the keyed block down and remounts). -->
{#key hintFlashKey}
<span
class="autosave-hint-flash absolute inset-0 rounded-md pointer-events-none"
aria-hidden="true"
></span>
{/key}
{/if}
<Popover
bind:isOpen={popoverOpen}
placement="bottom-end"
@@ -109,11 +197,11 @@
closeOnOutsideClick
>
{#snippet trigger()}
<div class='rounded-md p-1.5 hover:bg-surface-hover cursor-pointer'>
<div class="relative rounded-md p-1.5 hover:bg-surface-hover cursor-pointer">
{#if syncState === 'saving' || syncState === 'pending'}
<RefreshCcw size={14} class="animate-spin" />
{:else}
<CloudCheck size={16} />
<CloudCheck size={16} />
{/if}
</div>
{/snippet}
@@ -124,6 +212,21 @@
All changes are saved as a draft on the server. The draft is per-user — your teammates'
editors keep their own.
</p>
{#if othersDraftsCount > 0}
<div class="flex flex-col gap-2 border-t pt-3">
<p class="text-primary text-xs">
Other users are working on this {kindLabel}.
</p>
<Button
variant="default"
size="xs"
startIcon={{ icon: Users }}
on:click={openOthersDrafts}
>
See others' drafts
</Button>
</div>
{/if}
{#if showResetAction}
<Button
variant="default"
@@ -136,9 +239,26 @@
</Button>
{/if}
</div>
{/snippet}
{/snippet}
</Popover>
{#if label}
<span class="text-secondary text-2xs">{label}</span>
<span class="relative text-secondary text-2xs">{label}</span>
{/if}
</div>
</div>
<style>
/* Light-green fade behind the whole indicator when a load hint appears.
`forwards` keeps the end state (transparent) so the backdrop disappears
cleanly when the keyed wrapper unmounts. */
@keyframes autosave-hint-flash-anim {
0% {
background-color: rgba(34, 197, 94, 0.22);
}
100% {
background-color: transparent;
}
}
.autosave-hint-flash {
animation: autosave-hint-flash-anim 1.8s ease-out forwards;
}
</style>
@@ -125,6 +125,9 @@
onHistoryRestore,
onNavigate,
onResetToDeployed,
loadedFromDraft = false,
othersDraftsCount = 0,
onOpenOthersDrafts,
onTestJob
}: FlowBuilderProps = $props()
@@ -1088,6 +1091,9 @@
path={liveEditorDraftStoragePath}
draftOnly={newFlow}
{onResetToDeployed}
{loadedFromDraft}
{othersDraftsCount}
{onOpenOthersDrafts}
/>
{/if}
</div>
@@ -130,7 +130,10 @@
disableAi,
initialTestPanelCollapsed = false,
initialPathChosen = false,
onResetToDeployed
onResetToDeployed,
loadedFromDraft = false,
othersDraftsCount = 0,
onOpenOthersDrafts
}: ScriptBuilderProps = $props()
export function getInitialAndModifiedValues(): SavedAndModifiedValue {
@@ -1894,6 +1897,9 @@
path={userDraftPath}
draftOnly={(savedScript as any)?.no_deployed === true}
{onResetToDeployed}
{loadedFromDraft}
{othersDraftsCount}
{onOpenOthersDrafts}
/>
{/if}
</div>
@@ -79,7 +79,10 @@
gotoFn = (path: string, opt?: Record<string, any>) => window.history.pushState(null, '', path),
onSavedNewAppPath,
onNavigate,
onResetToDeployed
onResetToDeployed,
loadedFromDraft = false,
othersDraftsCount = 0,
onOpenOthersDrafts
}: AppEditorProps = $props()
migrateApp(untrack(() => app))
@@ -883,6 +886,9 @@
{newApp}
userDraftPath={appDraftPath}
{onResetToDeployed}
{loadedFromDraft}
{othersDraftsCount}
{onOpenOthersDrafts}
on:restore
{policy}
{fromHub}
@@ -109,6 +109,11 @@
// Threaded to the `AutosaveIndicator` popover so its "Reset to
// deployed" button can do the same thing the load-time toast offers.
onResetToDeployed?: () => void | Promise<void>
// See ScriptBuilderProps — same semantics for the app editor's
// indicator.
loadedFromDraft?: boolean
othersDraftsCount?: number
onOpenOthersDrafts?: () => void
}
let {
@@ -131,7 +136,10 @@
onHideRightPanel,
onHideBottomPanel,
onNavigate = undefined,
onResetToDeployed
onResetToDeployed,
loadedFromDraft = false,
othersDraftsCount = 0,
onOpenOthersDrafts
}: Props = $props()
/** Mirror of the path the user is editing in the pen popover. Initialized
@@ -950,6 +958,9 @@
path={userDraftPath}
draftOnly={newApp}
{onResetToDeployed}
{loadedFromDraft}
{othersDraftsCount}
{onOpenOthersDrafts}
/>
</div>
{/if}
@@ -167,6 +167,11 @@ export interface AppEditorProps {
// popover so its "Reset to deployed" button can do the same thing
// the load-time toast offers.
onResetToDeployed?: () => void | Promise<void>
// See ScriptBuilderProps — same semantics for the app editor's
// indicator. Threaded through AppEditorHeader.
loadedFromDraft?: boolean
othersDraftsCount?: number
onOpenOthersDrafts?: () => void
}
export type App = {
@@ -30,6 +30,10 @@
editPathFor: (forkedPath: string) => string
onLoadFromServer: () => void | Promise<void>
getLocalDraft: () => unknown
/** Bindable open-flag for the OtherUsersDraftsModal. The route owns
* the state (so the AutosaveIndicator popover button can flip it on)
* and binds it here. */
othersModalOpen: boolean
/** Defaults to true; set to false to suppress both modals. */
enabled?: boolean
}
@@ -42,6 +46,7 @@
editPathFor,
onLoadFromServer,
getLocalDraft,
othersModalOpen = $bindable(),
enabled = true
}: Props = $props()
</script>
@@ -61,6 +66,7 @@
currentUserUsername={$userStore?.username}
{otherDraftsUsers}
{editPathFor}
bind:isOpen={othersModalOpen}
/>
{/key}
{/if}
@@ -1,15 +1,15 @@
<script lang="ts">
/**
* Banner-style modal shown on editor mount when the deployed-overlay
* response carries `other_drafts_users` — i.e. someone other than the
* authed user (or the legacy NULL-email row) also has a saved draft at
* this path.
* Modal opened on demand (from the AutosaveIndicator popover or the
* home-page DraftBadge popover) when other workspace users have a
* draft at the same path. The owner list is part of the
* deployed-overlay / list payload; individual drafts are fetched
* on-demand for the "View JSON" / "Fork" actions so the response
* stays lean when many users are working on the same item.
*
* The list of owners is part of the get-by-path payload (so we don't
* fan out a second request just to populate the banner); individual
* drafts are fetched on-demand for the "View JSON" / "Fork" actions so
* the deploy-overlay response stays lean when many users are working
* on the same item.
* The parent controls visibility — bind to `isOpen`. The modal does
* NOT auto-open on mount; that used to surprise users every time
* they opened an item with collaborators.
*/
import { DraftService, type UserDraftItemKind } from '$lib/gen'
import { sendUserToast } from '$lib/toast'
@@ -37,12 +37,19 @@
* Different editors live under different roots (`/scripts/edit/`,
* `/flows/edit/`, ...) so the route owns the URL shape. */
editPathFor: (forkedPath: string) => string
/** Controlled visibility — bind from the parent. */
isOpen: boolean
}
let { workspace, itemKind, path, currentUserUsername, otherDraftsUsers, editPathFor }: Props =
$props()
let isOpen = $state(otherDraftsUsers.length > 0)
let {
workspace,
itemKind,
path,
currentUserUsername,
otherDraftsUsers,
editPathFor,
isOpen = $bindable()
}: Props = $props()
let busyFor = $state<string | null>(null)
let jsonOpen = $state(false)
let jsonOwnerLabel = $state('')
@@ -178,7 +185,7 @@
</ul>
<div class="flex justify-end">
<Button variant="default" size="sm" on:click={() => (isOpen = false)}>Continue anyway</Button>
<Button variant="default" size="sm" on:click={() => (isOpen = false)}>Close</Button>
</div>
</div>
</Modal2>
@@ -41,6 +41,11 @@ export type FlowBuilderProps = {
// Threaded to the `AutosaveIndicator` popover so its "Reset to
// deployed" button can do the same thing the load-time toast offers.
onResetToDeployed?: () => void | Promise<void>
// See ScriptBuilderProps — same semantics for the flow editor's
// indicator.
loadedFromDraft?: boolean
othersDraftsCount?: number
onOpenOthersDrafts?: () => void
// Fired whenever a test run is started from the flow editor, with the
// preview job id. Used by whitelabel embedders to track test jobs.
onTestJob?: (e: { jobId: string }) => void
@@ -97,6 +97,11 @@
// popover so its "Reset to deployed" button can do the same thing
// the load-time toast offers.
onResetToDeployed?: () => void | Promise<void>
// See ScriptBuilderProps — same semantics for the raw-app editor's
// indicator. Threaded through RawAppEditorHeader.
loadedFromDraft?: boolean
othersDraftsCount?: number
onOpenOthersDrafts?: () => void
}
let {
@@ -117,7 +122,10 @@
liveEditorDraftStoragePath = undefined,
defaultSplitWithPreview = true,
pendingDraftPath = $bindable(undefined),
onResetToDeployed
onResetToDeployed,
loadedFromDraft = false,
othersDraftsCount = 0,
onOpenOthersDrafts
}: Props = $props()
export const version: number | undefined = undefined
@@ -1405,6 +1413,9 @@
{onNavigate}
{onDeploy}
{onResetToDeployed}
{loadedFromDraft}
{othersDraftsCount}
{onOpenOthersDrafts}
canUndo={historyManager.canUndo}
canRedo={historyManager.canRedo}
onUndo={handleUndo}
@@ -136,6 +136,11 @@
// Threaded to the `AutosaveIndicator` popover so its "Reset to
// deployed" button can do the same thing the load-time toast offers.
onResetToDeployed?: () => void | Promise<void>
// See ScriptBuilderProps — same semantics for the raw-app editor's
// indicator.
loadedFromDraft?: boolean
othersDraftsCount?: number
onOpenOthersDrafts?: () => void
}
let {
@@ -164,7 +169,10 @@
liveEditorDraftStoragePath = undefined,
onDeploy = undefined,
pendingDraftPath = $bindable(undefined),
onResetToDeployed
onResetToDeployed,
loadedFromDraft = false,
othersDraftsCount = 0,
onOpenOthersDrafts
}: Props = $props()
$effect(() => {
@@ -731,6 +739,9 @@
path={liveEditorDraftStoragePath}
draftOnly={newApp}
{onResetToDeployed}
{loadedFromDraft}
{othersDraftsCount}
{onOpenOthersDrafts}
/>
{/if}
</div>
@@ -73,4 +73,13 @@ export interface ScriptBuilderProps {
// callers (session preview, embedded SDK) that shouldn't surface the
// action at all.
onResetToDeployed?: () => void | Promise<void>
// Triggers the AutosaveIndicator's on-mount "Loaded from draft" hint
// (with a one-shot green flash) the first time it flips to true.
loadedFromDraft?: boolean
// Non-zero when other workspace users have a draft at this path.
// Drives both the indicator's hint label ("Others are working on
// this script") and the popover's "See others' drafts" button.
othersDraftsCount?: number
// Wired by the route to flip the OtherUsersDraftsModal open.
onOpenOthersDrafts?: () => void
}
@@ -15,7 +15,6 @@
import { untrack } from 'svelte'
import { page } from '$app/state'
import { UserDraft } from '$lib/userDraft.svelte'
import { notifyDraftLoaded } from '$lib/userDraftToast'
let app = $state(undefined as (AppWithLastVersion & { value: any }) | undefined)
let savedApp:
@@ -37,6 +36,8 @@
* false once a deployed row exists at this path. */
let isNewApp = $state(false)
let otherDraftsUsers = $state<OtherDraftUser[]>([])
let loadedFromDraft = $state(false)
let othersModalOpen = $state(false)
/** Increments per `loadApp` call. Stale loads (e.g. when picker
* navigation races a draft-discard reload) bail at the next checkpoint
@@ -146,17 +147,7 @@
backendApp = { ...backendApp, value: savedDraftApp } as typeof backendApp
}
if (backendApp.is_draft) {
notifyDraftLoaded({
workspace: $workspaceStore!,
itemKind: 'app',
path: page.params.path ?? '',
draftOnly: backendApp.no_deployed,
onResetToDeployed: async () => {
UserDraft.remove('app', path)
await loadApp({ getDraft: false })
redraw++
}
})
loadedFromDraft = true
}
const backendApp_ = structuredClone(stateSnapshot(backendApp))
savedApp = {
@@ -237,6 +228,7 @@
redraw++
}}
getLocalDraft={() => app?.value}
bind:othersModalOpen
/>
{#key redraw}
@@ -266,6 +258,9 @@
await loadApp({ getDraft: false })
redraw++
}}
{loadedFromDraft}
othersDraftsCount={otherDraftsUsers.length}
onOpenOthersDrafts={() => (othersModalOpen = true)}
/>
</div>
{/if}
@@ -13,7 +13,7 @@
import { page } from '$app/state'
import { type RawAppData, DEFAULT_DATA } from '$lib/components/raw_apps/dataTableRefUtils'
import { UserDraft } from '$lib/userDraft.svelte'
import { armRestartOnFirstInteraction, notifyDraftLoaded } from '$lib/userDraftToast'
import { armRestartOnFirstInteraction } from '$lib/userDraftToast'
import DraftEditorModals from '$lib/components/common/confirmationModal/DraftEditorModals.svelte'
import { UserDraftDbSyncer } from '$lib/userDraftDbSyncer.svelte'
import { type OtherDraftUser } from '$lib/components/common/confirmationModal/OtherUsersDraftsModal.svelte'
@@ -138,6 +138,8 @@
* `updateApp` to `createApp` so a user-typed path is used. */
let isNewApp = $state(false)
let otherDraftsUsers = $state<OtherDraftUser[]>([])
let loadedFromDraft = $state(false)
let othersModalOpen = $state(false)
async function loadApp(opts: { getDraft?: boolean } = {}): Promise<void> {
const getDraft = opts.getDraft ?? true
const tok = ++loadAppToken
@@ -211,16 +213,7 @@
}
isNewApp = !!backendApp.no_deployed
if (backendApp.is_draft) {
notifyDraftLoaded({
workspace: $workspaceStore!,
itemKind: 'raw_app',
path: page.params.path ?? '',
draftOnly: backendApp.no_deployed,
onResetToDeployed: async () => {
draftHandle.draft = undefined
await loadApp({ getDraft: false })
}
})
loadedFromDraft = true
}
// Apply the user's saved draft. The autosave for raw apps writes a
// flat `RawAppDraft` (`{files, runnables, data, summary, policy,
@@ -369,6 +362,7 @@
editPathFor={(forkedPath) => `/apps_raw/edit/${forkedPath}`}
onLoadFromServer={() => loadApp()}
getLocalDraft={() => draftHandle.draft}
bind:othersModalOpen
/>
<RawAppTemplatePicker bind:open={templatePicker} onStart={onTemplatePickerStart} />
@@ -399,6 +393,9 @@
draftHandle.draft = undefined
await loadApp({ getDraft: false })
}}
{loadedFromDraft}
othersDraftsCount={otherDraftsUsers.length}
onOpenOthersDrafts={() => (othersModalOpen = true)}
/>
</div>
{/key}
@@ -19,7 +19,7 @@
import type { stepState } from '$lib/components/stepHistoryLoader.svelte'
import { page } from '$app/state'
import { UserDraft } from '$lib/userDraft.svelte'
import { armRestartOnFirstInteraction, notifyDraftLoaded } from '$lib/userDraftToast'
import { armRestartOnFirstInteraction } from '$lib/userDraftToast'
let version: undefined | number = $state(undefined)
@@ -39,6 +39,8 @@
let savedFlow: Flow | undefined = $state(undefined)
let otherDraftsUsers = $state<OtherDraftUser[]>([])
let loadedFromDraft = $state(false)
let othersModalOpen = $state(false)
// `initialPath` is the editor-displayed path. Defaults to the URL
// path so a deployed (or existing-draft) reload mounts FlowBuilder
// with the real path; cleared to '' inside the `new_draft` branch
@@ -215,16 +217,7 @@
// lands at this URL path.
isNewFlow = !!backendFlow.no_deployed
if (backendFlow.is_draft) {
notifyDraftLoaded({
workspace: $workspaceStore!,
itemKind: 'flow',
path: page.params.path ?? '',
draftOnly: backendFlow.no_deployed,
onResetToDeployed: async () => {
flowHandle.draft = undefined
await loadFlow({ getDraft: false })
}
})
loadedFromDraft = true
}
// `backendFlow` is the deployed payload; the user's saved draft
// (if any) is attached as `.draft`. Layer the draft over the
@@ -315,6 +308,7 @@
editPathFor={(forkedPath) => `/flows/edit/${forkedPath}`}
onLoadFromServer={() => loadFlow()}
getLocalDraft={() => flowHandle.draft}
bind:othersModalOpen
/>
{#if notFound}
<div class="flex flex-col items-center justify-center h-full">
@@ -338,6 +332,9 @@
flowHandle.draft = undefined
await loadFlow({ getDraft: false })
}}
{loadedFromDraft}
othersDraftsCount={otherDraftsUsers.length}
onOpenOthersDrafts={() => (othersModalOpen = true)}
onNavigate={(item) => goto(editPathFor(item))}
{flowStore}
{flowStateStore}
@@ -16,7 +16,6 @@
import { untrack } from 'svelte'
import { page } from '$app/state'
import { UserDraft } from '$lib/userDraft.svelte'
import { notifyDraftLoaded } from '$lib/userDraftToast'
import { UserDraftDbSyncer } from '$lib/userDraftDbSyncer.svelte'
type EditableScript = NewScript & { draft_triggers?: Trigger[] }
@@ -67,8 +66,16 @@
let fullyLoaded = $state(false)
/** Other workspace users (and the legacy NULL-email row, if any) with
* a draft on this path. Populated from the deployed-overlay response
* on each `loadScript`; the banner opens when the list is non-empty. */
* on each `loadScript`; the AutosaveIndicator picks up the count for
* its on-mount "Others are working on this script" hint. */
let otherDraftsUsers = $state<OtherDraftUser[]>([])
/** Whether the editor mounted on a per-user draft this load — flipped
* true once when the overlay response says so, drives the
* AutosaveIndicator's on-mount "Loaded from draft" hint. */
let loadedFromDraft = $state(false)
/** Bound through DraftEditorModals; flipped on from the
* AutosaveIndicator popover's "See others' drafts" button. */
let othersModalOpen = $state(false)
// Remounts ScriptBuilder on nav: false while a reload runs, true once data is
// ready. A synchronous `{#key}` swap instead races Monaco's init against the
@@ -161,19 +168,7 @@
)
}
if (backendScript.is_draft) {
notifyDraftLoaded({
workspace: $workspaceStore!,
itemKind: 'script',
path: page.params.path ?? '',
draftOnly: backendScript.no_deployed,
onResetToDeployed: async () => {
// Drop the in-memory draft and refetch *without* the
// draft overlay — we don't trust the eventual delete
// to have landed, so we read deployed directly.
scriptHandle.draft = undefined
await loadScript({ getDraft: false })
}
})
loadedFromDraft = true
}
// `backendScript` is the deployed payload; the user's saved
// draft (if any) sits in `.draft`. Layer the draft over the
@@ -249,6 +244,7 @@
editPathFor={(forkedPath) => `/scripts/edit/${forkedPath}`}
onLoadFromServer={() => loadScript()}
getLocalDraft={() => scriptHandle.draft}
bind:othersModalOpen
/>
{#if scriptHandle.draft && renderEditor}
<ScriptBuilder
@@ -262,6 +258,9 @@
{diffDrawer}
{savedPrimarySchedule}
searchParams={page.url.searchParams}
{loadedFromDraft}
othersDraftsCount={otherDraftsUsers.length}
onOpenOthersDrafts={() => (othersModalOpen = true)}
onResetToDeployed={async () => {
scriptHandle.draft = undefined
await loadScript({ getDraft: false })