fix: the picker marks the version on display as head, restore compares the head, relocation follows the last move

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Ruben Fiszel
2026-09-15 01:14:59 +02:00
co-authored by Claude Opus 5
parent 8146737342
commit 1c035e44ab
4 changed files with 34 additions and 6 deletions
@@ -93,6 +93,10 @@
let selectedVersion: string | undefined = $state(undefined)
let versionLoader: ((id: string) => Promise<Value | undefined>) | undefined = $state(undefined)
let headLabel: string | undefined = $state(undefined)
/** The deployed head as it was handed to `setDiff`. `data.deployed` follows the
* picker, and Restore always restores the head, so its enabled state compares
* against this rather than whichever version is on display. */
let headDeployed: Value | undefined = $state(undefined)
let loadingVersion = $state(false)
/** Which version load the spinner belongs to, counted rather than keyed on the id so
* re-picking the same version is still generation-safe. A response from any other
@@ -176,6 +180,7 @@
} = diff
versionLoader = loadVersion
headLabel = deployedLabel
headDeployed = !deployed.draft_only ? prepareDiff(deployed) : undefined
// A load still in flight belongs to the diff being replaced.
versionLoadGeneration++
loadingVersion = false
@@ -333,7 +338,8 @@
variant="default"
onClick={restoreDeployed}
disabled={!data.draft &&
orderedJsonStringify(data.deployed) === orderedJsonStringify(data.current)}
orderedJsonStringify(headDeployed ?? data.deployed) ===
orderedJsonStringify(data.current)}
>
Restore to deployed{data.draft ? ' and discard draft' : ''}
</Button>
@@ -189,6 +189,9 @@
// Used by multiplayer deploy collision warning
let deployedValue: Value | undefined = $state(undefined) // Value to diff against
let deployedLabel: string | undefined = $state(undefined) // Names it in the diff
/** The flow_version the payload in `deployedValue` came from, so the picker marks that
* one as head rather than trusting the history's first row. */
let deployedVersionShown: number | undefined = $state(undefined)
let deployedBy: string | undefined = $state(undefined) // Author
let confirmCallback: () => void = $state(() => {}) // What happens when user clicks `override` in warning
let open: boolean = $state(false) // Is confirmation modal open
@@ -486,6 +489,7 @@
workspace_id: undefined
})
deployedBy = flow.edited_by
deployedVersionShown = flow.version_id
// Names the deployed side of the diff. Without it the reader is shown two panes
// and told nothing about what the left one is.
deployedLabel = `Deployed${flow.version_id != null ? ` ${flow.version_id}` : ''}${flow.edited_by ? ` by ${flow.edited_by}` : ''} · latest`
@@ -1136,13 +1140,17 @@
try {
const history = await FlowService.getFlowHistory({ workspace: opWorkspace, path })
const total = history.length
// Head is the version the payload beside this list came from, not whatever the
// history now leads with: a deploy landing between the two fetches would
// otherwise label the shown (older) value as the latest.
const head = deployedVersionShown ?? history[0]?.id
return history.map((h, i) => {
const detail = [
h.created_by,
h.created_at ? new Date(h.created_at).toLocaleString() : undefined,
h.deployment_msg
].filter(Boolean)
const isHead = i === 0
const isHead = h.id === head
return {
id: String(h.id),
label: `v${total - i} · ${h.id}${isHead ? ' · latest' : ''}`,
@@ -141,9 +141,12 @@
const seg = EDITOR_SEGMENT[itemKind]
if (!seg) return
const query = { workspace, itemKind, path }
// The flush below saves again and can land here a second time.
// The flush below saves again and can land here a second time, and a second move
// can land while it runs: the last destination reported is the one to follow.
let relocating = false
let destination: string | undefined = undefined
return UserDraftDbSyncer.onRelocated(query, async (newPath) => {
destination = newPath
if (relocating) return
relocating = true
await onBeforeRelocate?.()
@@ -158,8 +161,9 @@
relocating = false
return
}
sendUserToast(`This item was moved to ${newPath}. You are now editing it there.`)
await goto(`${base}/${seg}/${newPath}`)
const target = destination ?? newPath
sendUserToast(`This item was moved to ${target}. You are now editing it there.`)
await goto(`${base}/${seg}/${target}`)
})
})
</script>
@@ -401,12 +401,20 @@
})
deployedBy = deployedApp.created_by
const shownVersions = (deployedApp as { versions?: number[] }).versions
deployedVersionShown = Array.isArray(shownVersions)
? shownVersions[shownVersions.length - 1]
: undefined
// Normalize away post-deploy noise (see stripRawAppDiffNoise) so the
// diff/comparison only reflects what the editor actually changed.
deployedValue = replaceFalseWithUndefined(stripRawAppDiffNoise(deployedApp))
}
/** The app_version the payload in `deployedValue` came from, so the picker marks that
* one as head rather than trusting the history's first row. */
let deployedVersionShown: number | undefined = $state(undefined)
/** Deployed versions for the diff picker, newest first. Best-effort: losing the
* list costs the picker, not the diff. */
async function deployedVersionOptions() {
@@ -417,13 +425,15 @@
path: appPath
})
const total = history.length
// Head is the version the payload beside this list came from; see FlowBuilder.
const head = deployedVersionShown ?? history[0]?.version
return history.map((h, i) => {
const detail = [
h.created_by,
h.created_at ? new Date(h.created_at).toLocaleString() : undefined,
h.deployment_msg
].filter(Boolean)
const isHead = i === 0
const isHead = h.version === head
return {
id: String(h.version),
label: `v${total - i} · ${h.version}${isHead ? ' · latest' : ''}`,