mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-10-13 16:02:59 +00:00
fix(editors): keep a save out of the toggle's field, and report only once every workspace has settled
An update sends no `enabled` — the toggle deploys that field by itself — so a save adopting its click-time snapshot wholesale rewound a toggle that had landed meanwhile: the form called the deployed state unsaved, and Discard put the old one back. The baseline takes the field from the toggle, not from the snapshot. A variable save reported each workspace as it landed, and the first report retargets the host, whose keying unmounts the editor at the next workspace's await — releasing the handles the workspaces after it settle against, so their drafts survived a successful save. Reports wait for the loop to finish. The list frames now share one debounced reload queue with the chat path, rather than reloading once per reason for one deploy. 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
5b99cce175
commit
eb80261258
@@ -299,6 +299,10 @@
|
||||
// linked workspace, and a rename made there is that workspace's alone.
|
||||
const actingPath = payloads.find((pl) => pl.ws === fromWs)?.s.path ?? from
|
||||
let actingCommitted = false
|
||||
// Every workspace written, with the path it wrote there. Reported once the loop
|
||||
// is done: per workspace, each carrying its own, so a linked workspace's rename
|
||||
// moves the tabs acting on it and no others.
|
||||
const written: { ws: string; path: string }[] = []
|
||||
try {
|
||||
for (const { ws, s, ini, existed } of payloads) {
|
||||
if (existed) {
|
||||
@@ -346,12 +350,10 @@
|
||||
await settleDraftAfterWrite('variable', s, states[ws]?.draft, from ?? '', s.path, {
|
||||
workspace: ws
|
||||
})
|
||||
// Reported after settling, so what hears about the write sees a cell that
|
||||
// has already been resolved. Per workspace, each carrying its own and the
|
||||
// path it wrote there, so a linked workspace's rename moves the tabs acting
|
||||
// on it and no others — and a workspace written before a later one threw is
|
||||
// still reported, because it is deployed.
|
||||
onSaved?.(s.path, from, ws)
|
||||
// Collected, not reported yet: a report retargets the host, whose `{#key
|
||||
// path}` would unmount this editor at the next workspace's await and leave
|
||||
// that one's handle released — its draft then settles against nothing.
|
||||
written.push({ ws, path: s.path })
|
||||
// Path now exists server-side — drop the autocomplete cache so
|
||||
// it shows up immediately instead of after the 60s TTL.
|
||||
invalidateWorkspacePaths(ws)
|
||||
@@ -360,6 +362,7 @@
|
||||
edit ? `Updated variable in ${payloads.length} workspace(s)` : `Created variable`
|
||||
)
|
||||
dispatch('create')
|
||||
for (const w of written) onSaved?.(w.path, from, w.ws)
|
||||
// Only while this editor is still the one that was saved: re-pointed,
|
||||
// `editPath` is the variable it moved to.
|
||||
if (editPath === from) {
|
||||
@@ -370,6 +373,8 @@
|
||||
}
|
||||
} catch (err) {
|
||||
sendUserToast(`Could not save variable: ${err.body}`, true)
|
||||
// Whatever committed before the throw is deployed, so it is still reported.
|
||||
for (const w of written) onSaved?.(w.path, from, w.ws)
|
||||
// Reopened, so the handles come from the server: re-keying them would bring
|
||||
// back the values this drawer opened on, which describe neither what the
|
||||
// committed workspace now has deployed nor the draft the failed one still
|
||||
|
||||
@@ -638,6 +638,10 @@
|
||||
// remounting: the form stays editable during the write, and a remount would
|
||||
// re-read over an edit made then — which `settleDraftAfterWrite` keeps.
|
||||
initialConfig = structuredClone(scheduleCfg)
|
||||
// An update's payload carries no `enabled` (see saveScheduleFromCfg): the
|
||||
// toggle deploys that field on its own, so the deployed value is whatever it
|
||||
// last set, not what this save's click-time snapshot happens to hold.
|
||||
if (!wasCreate) initialConfig.enabled = enabled
|
||||
// An edit made while the write was in flight is kept rather than settled away
|
||||
// — and the host must not remount over it, which is the only thing that can
|
||||
// tell it so.
|
||||
|
||||
@@ -295,10 +295,9 @@ export async function settleDraftAfterWrite<V>(
|
||||
}
|
||||
|
||||
/**
|
||||
* Send whatever this cell has parked on the autosave debounce, now. For a reader
|
||||
* that cannot be corrected once it has read — a list page inside an iframe, whose
|
||||
* draft markers come from its own store — and would otherwise render a row the
|
||||
* debounce has not caught up with.
|
||||
* Send whatever this cell has parked on the autosave debounce, now. For a caller
|
||||
* that has to know the cell is resolved before it acts — settling after a write,
|
||||
* before anything reports or remounts on it.
|
||||
*/
|
||||
export async function flushDraftWrites(
|
||||
itemKind: UserDraftItemKind,
|
||||
|
||||
@@ -636,9 +636,10 @@
|
||||
const entity = parseEntityEditorRoute(loc)
|
||||
// A bare list tab shows every row, so any write on its page is its business
|
||||
// — same as for a chat mutation, and its rows and `*` markers go stale
|
||||
// otherwise.
|
||||
// otherwise. Queued rather than reloaded here, so one deploy's writes and
|
||||
// this report collapse into a single reload.
|
||||
if (!entity) {
|
||||
if (mountedTabKeys.has(key)) tabHosts[key]?.reload()
|
||||
queuePageReload(page)
|
||||
continue
|
||||
}
|
||||
if (entity.path !== ev.path) continue
|
||||
@@ -654,6 +655,14 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
// One queue for every reason a previewed list page goes stale — a chat tool, a
|
||||
// hosted editor's write, a draft landing afterwards — so the same frame is not
|
||||
// reloaded several times for one deploy.
|
||||
function queuePageReload(page: string) {
|
||||
pendingPages.add(page)
|
||||
clearTimeout(reloadHandle)
|
||||
reloadHandle = setTimeout(flushReload, 500)
|
||||
}
|
||||
function flushReload() {
|
||||
const pages = pendingPages
|
||||
const mutations = pendingMutations
|
||||
@@ -665,37 +674,11 @@
|
||||
// nothing we do afterwards corrects what it already read. Rather than time our
|
||||
// reloads against writes, reload when one lands: a draft written after a save —
|
||||
// an edit made while it was in flight — reaches the list this way too.
|
||||
let draftLandedHandle: ReturnType<typeof setTimeout> | undefined
|
||||
let draftLandedPages = new Set<string>()
|
||||
$effect(() => {
|
||||
const stop = UserDraftDbSyncer.onAnySaved(({ workspace, itemKind }) => {
|
||||
return UserDraftDbSyncer.onAnySaved(({ itemKind }) => {
|
||||
const page = entityListPage(itemKind as EntityEditorKind)?.path
|
||||
if (!page) return
|
||||
draftLandedPages.add(`${workspace}\u0000${page}`)
|
||||
clearTimeout(draftLandedHandle)
|
||||
// Coalesced: a deploy lands several writes for one item back to back.
|
||||
draftLandedHandle = setTimeout(() => {
|
||||
const pages = draftLandedPages
|
||||
draftLandedPages = new Set()
|
||||
for (const s of warmSessions) {
|
||||
const ws = getEffectiveWorkspaceId(s)
|
||||
const owner = getRuntime(s.id)?.previewTabs
|
||||
if (!ws || !owner) continue
|
||||
for (const tab of owner.tabs) {
|
||||
const loc = whereIs(tab)
|
||||
if (parseEntityEditorRoute(loc)) continue
|
||||
if (!pages.has(`${ws}\u0000${stripBase(loc)}`)) continue
|
||||
const key = tabKey(s.id, tab.id)
|
||||
if (mountedTabKeys.has(key)) tabHosts[key]?.reload()
|
||||
}
|
||||
}
|
||||
}, 300)
|
||||
if (page) queuePageReload(page)
|
||||
})
|
||||
return () => {
|
||||
clearTimeout(draftLandedHandle)
|
||||
draftLandedPages = new Set()
|
||||
stop()
|
||||
}
|
||||
})
|
||||
|
||||
$effect(() => {
|
||||
|
||||
Reference in New Issue
Block a user