mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-10-13 16:02:59 +00:00
fix(sessions): leave a hosted editor whose item a direct discard removed, and follow a renamed save
The inline draft banner's Discard calls UserDraft.discard straight off, not the discard_local_draft tool, so nothing told the tab about it. For a draft-only item the draft IS the item: discarding deletes its only representation and the tab kept editing a synthesized stand-in for something that no longer exists. Each editor now reports whether the item survived, and the session views hand that back to the tab, which returns to the list it was opened from. A draft-only schedule also opens with its path editable — saving CREATEs — so the save can land somewhere other than where the tab points. The schedule host discarded onUpdate's saved path and only remounted, re-opening the old path; it now retargets the tab like the resource and variable hosts do. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KcyBRxE8j3ujCYkum9KrC4
This commit is contained in:
co-authored by
Claude Opus 5
parent
947fd6b1cb
commit
420beeadd2
@@ -290,11 +290,16 @@
|
||||
export function localDraftCurrent(): ResourceState | undefined {
|
||||
return current
|
||||
}
|
||||
export function discardLocalDraft(): void {
|
||||
if (!selected) return
|
||||
/** Returns whether the resource still exists afterwards. Discarding a draft
|
||||
* that was never deployed removes the resource itself — `initialStates` holds a
|
||||
* synthesized stand-in, not a baseline to fall back to — so a caller showing it
|
||||
* has to stop rather than keep displaying that stand-in. */
|
||||
export function discardLocalDraft(): boolean {
|
||||
if (!selected) return true
|
||||
UserDraft.discard('resource', initialPath ?? '', initialStates[selected], {
|
||||
workspace: selected
|
||||
})
|
||||
return !!existedInitially[selected]
|
||||
}
|
||||
|
||||
$effect(() => {
|
||||
|
||||
@@ -25,7 +25,8 @@
|
||||
onRestored = undefined,
|
||||
onSaved = undefined,
|
||||
useDrawer = true,
|
||||
onBack = undefined
|
||||
onBack = undefined,
|
||||
onRemoved = undefined
|
||||
}: {
|
||||
workspace?: string
|
||||
disableChatOffset?: boolean
|
||||
@@ -44,6 +45,9 @@
|
||||
/** Inline only: offered in the header when the host replaced something the
|
||||
* user should be able to get back to (a session tab that took over the list). */
|
||||
onBack?: () => void
|
||||
/** The resource is gone — a draft-only one whose draft was discarded. A host
|
||||
* addressing it by path (a session tab) has to stop showing it. */
|
||||
onRemoved?: () => void
|
||||
} = $props()
|
||||
|
||||
let drawer: Drawer | undefined = $state()
|
||||
@@ -61,7 +65,8 @@
|
||||
save: () => Promise<boolean>
|
||||
localDraftDeployed: () => unknown
|
||||
localDraftCurrent: () => unknown
|
||||
discardLocalDraft: () => void
|
||||
/** False when the discard removed the resource (it was draft-only). */
|
||||
discardLocalDraft: () => boolean
|
||||
}
|
||||
| undefined = $state(undefined)
|
||||
let hasLocalDraft = $state(false)
|
||||
@@ -171,7 +176,9 @@
|
||||
reserveSpace={mode == 'edit'}
|
||||
getDeployed={() => resourceEditor?.localDraftDeployed()}
|
||||
getCurrent={() => resourceEditor?.localDraftCurrent()}
|
||||
onDiscard={() => resourceEditor?.discardLocalDraft()}
|
||||
onDiscard={() => {
|
||||
if (resourceEditor?.discardLocalDraft() === false) onRemoved?.()
|
||||
}}
|
||||
disabled={!canWriteSelected}
|
||||
/>
|
||||
{/snippet}
|
||||
|
||||
@@ -43,7 +43,8 @@
|
||||
workspace = undefined,
|
||||
useDrawer = true,
|
||||
onBack = undefined,
|
||||
onSaved = undefined
|
||||
onSaved = undefined,
|
||||
onRemoved = undefined
|
||||
}: {
|
||||
workspace?: string
|
||||
/**
|
||||
@@ -59,6 +60,9 @@
|
||||
/** Fires after a save, with the path it wrote to — which is not the one it was
|
||||
* opened on when the user renamed it. */
|
||||
onSaved?: (savedPath?: string) => void
|
||||
/** The variable is gone — a draft-only one whose draft was discarded. A host
|
||||
* addressing it by path (a session tab) has to stop showing it. */
|
||||
onRemoved?: () => void
|
||||
} = $props()
|
||||
let curWs = $derived(workspace ?? $workspaceStore)
|
||||
|
||||
@@ -343,6 +347,10 @@
|
||||
UserDraft.discard('variable', editPath ?? '', initialStates[selected], {
|
||||
workspace: selected
|
||||
})
|
||||
// A draft-only variable has no deployed row under the draft, so discarding
|
||||
// it removed the variable: `initialStates` holds a synthesized stand-in,
|
||||
// not a baseline to fall back to.
|
||||
if (!existedInitially[selected]) onRemoved?.()
|
||||
}}
|
||||
disabled={!can_write}
|
||||
/>
|
||||
|
||||
@@ -373,7 +373,12 @@
|
||||
{#await import('./ScheduleEditorView.svelte')}
|
||||
{@render editorLoading()}
|
||||
{:then Module}
|
||||
<Module.default path={slot.path} {workspaceId} onBack={backToList} />
|
||||
<Module.default
|
||||
path={slot.path}
|
||||
{workspaceId}
|
||||
onBack={backToList}
|
||||
onRenamed={retargetTo}
|
||||
/>
|
||||
{/await}
|
||||
{:else if slot.entityKind === 'resource'}
|
||||
{#await import('./ResourceEditorView.svelte')}
|
||||
|
||||
@@ -38,6 +38,7 @@
|
||||
useDrawer={false}
|
||||
workspace={workspaceId}
|
||||
{onBack}
|
||||
onRemoved={onBack}
|
||||
onSaved={(saved) => {
|
||||
if (saved && saved !== path) onRenamed?.(saved)
|
||||
}}
|
||||
|
||||
@@ -8,7 +8,8 @@
|
||||
let {
|
||||
path,
|
||||
workspaceId,
|
||||
onBack
|
||||
onBack,
|
||||
onRenamed
|
||||
}: {
|
||||
/** The schedule this tab edits (the row its location deep-links). */
|
||||
path: string
|
||||
@@ -17,6 +18,10 @@
|
||||
workspaceId: string
|
||||
/** Back to the list this editor was reached through; the tab replaced it. */
|
||||
onBack?: () => void
|
||||
/** The item was saved under a different path. The tab addresses it by path —
|
||||
* as do its label, the chat's ACTIVE PREVIEW and the draft key — so the tab
|
||||
* has to follow, or all four keep naming an item that no longer exists. */
|
||||
onRenamed?: (newPath: string) => void
|
||||
} = $props()
|
||||
|
||||
// Captured at init, so it must read the current prop rather than close over it.
|
||||
@@ -59,7 +64,13 @@
|
||||
bind:this={editor}
|
||||
useDrawer={false}
|
||||
showDraftBanner
|
||||
onUpdate={() => generation++}
|
||||
onRemoved={onBack}
|
||||
onUpdate={(saved: string | undefined) => {
|
||||
generation++
|
||||
// A draft-only schedule opens with its path editable (saving CREATEs),
|
||||
// so the save can land somewhere other than where the tab is pointed.
|
||||
if (saved && saved !== path) onRenamed?.(saved)
|
||||
}}
|
||||
>
|
||||
{#snippet customLabel()}
|
||||
<div class="flex flex-row items-center gap-2 min-w-0">
|
||||
|
||||
@@ -38,6 +38,7 @@
|
||||
useDrawer={false}
|
||||
workspace={workspaceId}
|
||||
{onBack}
|
||||
onRemoved={onBack}
|
||||
onSaved={(saved) => {
|
||||
if (saved && saved !== path) onRenamed?.(saved)
|
||||
}}
|
||||
|
||||
@@ -68,7 +68,10 @@
|
||||
// false) it is the host's, since the trigger panel inside a script or flow
|
||||
// editor is covered by that editor's banner. A host that stands alone — a
|
||||
// session's schedule tab — opts in, or nothing says an edit is unsaved.
|
||||
showDraftBanner = false
|
||||
showDraftBanner = false,
|
||||
// The schedule is gone — a draft-only one whose draft the banner discarded.
|
||||
// A host addressing it by path (a session tab) has to stop showing it.
|
||||
onRemoved = undefined
|
||||
} = $props()
|
||||
|
||||
let optionTabSelected:
|
||||
@@ -79,6 +82,9 @@
|
||||
| 'dynamic_skip' = $state('error_handler')
|
||||
let initialPath = $state('')
|
||||
let edit = $state(true)
|
||||
// Opened on a schedule that exists only as a draft: its draft IS the schedule,
|
||||
// so discarding it removes the item rather than reverting it.
|
||||
let draftOnly = $state(false)
|
||||
let schedule: string = $state('0 0 12 * *')
|
||||
let cronVersion: string = $state('v2')
|
||||
let isLatestCron = $state(true)
|
||||
@@ -183,6 +189,7 @@
|
||||
const { overlay: draftOverlay, noDeployed } = await loadSchedule(defaultCfg)
|
||||
// Draft-only schedules have no deployed row, so saving must CREATE (update 404s).
|
||||
edit = !noDeployed
|
||||
draftOnly = noDeployed
|
||||
if (!defaultCfg) {
|
||||
// Form holds DEPLOYED here; capture it as `initialConfig` so the
|
||||
// dirty check / banner fires whenever a saved draft exists.
|
||||
@@ -339,6 +346,7 @@
|
||||
drawer?.openDrawer()
|
||||
runnable = undefined
|
||||
edit = false
|
||||
draftOnly = false
|
||||
// No deployed baseline for a brand-new schedule. The editor instance
|
||||
// is reused across open() calls, so clear any baseline left by a prior
|
||||
// openEdit — otherwise the "unsaved changes" banner / dirty check would
|
||||
@@ -1455,7 +1463,10 @@
|
||||
getDeployed={() => draftSync.deployed}
|
||||
reserveSpace={draftSync.hasBaseline}
|
||||
getCurrent={() => draftSync.current}
|
||||
onDiscard={() => draftSync.resetToDeployed(initialPath)}
|
||||
onDiscard={async () => {
|
||||
await draftSync.resetToDeployed(initialPath)
|
||||
if (draftOnly) onRemoved?.()
|
||||
}}
|
||||
disabled={!can_write}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
Reference in New Issue
Block a user