From 1c035e44ab6cf3ceda17d23ffa0f5f8161658a0b Mon Sep 17 00:00:00 2001 From: Ruben Fiszel Date: Tue, 15 Sep 2026 01:14:59 +0200 Subject: [PATCH] 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) --- frontend/src/lib/components/DiffDrawer.svelte | 8 +++++++- frontend/src/lib/components/FlowBuilder.svelte | 10 +++++++++- .../confirmationModal/DraftEditorModals.svelte | 10 +++++++--- .../components/raw_apps/RawAppEditorHeader.svelte | 12 +++++++++++- 4 files changed, 34 insertions(+), 6 deletions(-) diff --git a/frontend/src/lib/components/DiffDrawer.svelte b/frontend/src/lib/components/DiffDrawer.svelte index 1f92d95477..e245e30e19 100644 --- a/frontend/src/lib/components/DiffDrawer.svelte +++ b/frontend/src/lib/components/DiffDrawer.svelte @@ -93,6 +93,10 @@ let selectedVersion: string | undefined = $state(undefined) let versionLoader: ((id: string) => Promise) | 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' : ''} diff --git a/frontend/src/lib/components/FlowBuilder.svelte b/frontend/src/lib/components/FlowBuilder.svelte index eccdbefff4..3f5d8bc87c 100644 --- a/frontend/src/lib/components/FlowBuilder.svelte +++ b/frontend/src/lib/components/FlowBuilder.svelte @@ -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' : ''}`, diff --git a/frontend/src/lib/components/common/confirmationModal/DraftEditorModals.svelte b/frontend/src/lib/components/common/confirmationModal/DraftEditorModals.svelte index 3b62b07101..20c29286b5 100644 --- a/frontend/src/lib/components/common/confirmationModal/DraftEditorModals.svelte +++ b/frontend/src/lib/components/common/confirmationModal/DraftEditorModals.svelte @@ -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}`) }) }) diff --git a/frontend/src/lib/components/raw_apps/RawAppEditorHeader.svelte b/frontend/src/lib/components/raw_apps/RawAppEditorHeader.svelte index a865b46cdc..8799df9310 100644 --- a/frontend/src/lib/components/raw_apps/RawAppEditorHeader.svelte +++ b/frontend/src/lib/components/raw_apps/RawAppEditorHeader.svelte @@ -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' : ''}`,