diff --git a/frontend/src/lib/components/DiffDrawer.svelte b/frontend/src/lib/components/DiffDrawer.svelte index edb8b1e9dd..01b8c67d1d 100644 --- a/frontend/src/lib/components/DiffDrawer.svelte +++ b/frontend/src/lib/components/DiffDrawer.svelte @@ -51,7 +51,8 @@ * way to tell what their draft is being compared against. */ deployedLabel?: string versions?: DiffVersionOption[] - onTakeLatest?: () => void | Promise + onTakeLatest?: (head?: string) => void | Promise + draftBase?: string draft: DiffData | undefined current: DiffData path?: string @@ -139,12 +140,24 @@ } } + /** The version this drawer presents as the deployed head, when it has a list to say + * so. Both the action's gate and the base it adopts hang off it, so "take latest" + * means the version the reader is looking at. */ + const headShown = $derived.by(() => + data?.mode === 'normal' ? data.versions?.find((v) => v.isHead)?.id : undefined + ) + /** Behind as the drawer can see it: a base that is not the head on display. With no + * version list there is nothing to compare, so the editor's own gate stands. */ + const behindShown = $derived.by( + () => data?.mode === 'normal' && (headShown == null || data.draftBase !== headShown) + ) + let takingLatest = $state(false) async function takeLatest() { if (!data || data.mode !== 'normal' || !data.onTakeLatest || takingLatest) return takingLatest = true try { - await data.onTakeLatest() + await data.onTakeLatest(headShown) diffViewer?.closeDrawer() } finally { takingLatest = false @@ -159,7 +172,8 @@ deployedLabel?: string versions?: DiffVersionOption[] loadVersion?: (id: string) => Promise - onTakeLatest?: () => void | Promise + onTakeLatest?: (head?: string) => void | Promise + draftBase?: string draft?: Value | undefined current: Value defaultDiffType?: 'deployed' | 'draft' @@ -180,6 +194,7 @@ versions, loadVersion, onTakeLatest, + draftBase, draft, current, button @@ -197,6 +212,7 @@ deployedLabel, versions, onTakeLatest, + draftBase, draft: draft ? prepareDiff(draft) : undefined, current: prepareDiff(current), path: draft?.path || deployed?.path, @@ -333,7 +349,7 @@ {/if} {#snippet actions()} - {#if data?.mode === 'normal' && data.onTakeLatest} + {#if data?.mode === 'normal' && data.onTakeLatest && behindShown} diff --git a/frontend/src/lib/components/FlowBuilder.svelte b/frontend/src/lib/components/FlowBuilder.svelte index 3f5d8bc87c..668135db37 100644 --- a/frontend/src/lib/components/FlowBuilder.svelte +++ b/frontend/src/lib/components/FlowBuilder.svelte @@ -1175,6 +1175,7 @@ deployedLabel, versions: await deployedVersionOptions(), onTakeLatest, + draftBase: draftBaseVersion, loadVersion: async (id) => { const v = await FlowService.getFlowVersion({ workspace: opWorkspace!, diff --git a/frontend/src/lib/components/ScriptBuilder.svelte b/frontend/src/lib/components/ScriptBuilder.svelte index c435baf18a..91b81b2d22 100644 --- a/frontend/src/lib/components/ScriptBuilder.svelte +++ b/frontend/src/lib/components/ScriptBuilder.svelte @@ -120,6 +120,7 @@ initialPath = $bindable(''), userDraftPath = '', onTakeLatest = undefined, + draftBaseHash = undefined, autosaveWorkspace = undefined, autosavePath = undefined, template = $bindable('script'), @@ -898,6 +899,7 @@ deployedLabel: deployedVersionLabel(deployed), versions: await deployedVersionOptions(headHash), onTakeLatest, + draftBase: draftBaseHash, loadVersion: async (hash) => { const v = await ScriptService.getScriptByHash({ workspace: opWorkspace!, hash }) return replaceFalseWithUndefined({ diff --git a/frontend/src/lib/components/diff_drawer.ts b/frontend/src/lib/components/diff_drawer.ts index 60a125ec0b..fd3222f8f9 100644 --- a/frontend/src/lib/components/diff_drawer.ts +++ b/frontend/src/lib/components/diff_drawer.ts @@ -27,10 +27,14 @@ export type DiffDrawerDiff = /** Loads one version's payload. Returning `undefined` leaves the current * comparison in place rather than blanking the diff. */ loadVersion?: (id: string) => Promise - /** Moves the draft's base to the head and keeps its content. Passed only - * while the draft is behind; rendered as a header action so the user - * takes the latest with the diff in front of them. */ - onTakeLatest?: () => void | Promise + /** Moves the draft's base to the head and keeps its content, rendered as a + * header action so the user takes the latest with the diff in front of them. + * Called with the version the drawer is showing as head (from `versions`), so + * the base adopted is the one the reader just looked at. */ + onTakeLatest?: (head?: string) => void | Promise + /** The version the draft forked from. The drawer offers `onTakeLatest` only + * while it differs from the head on display. */ + draftBase?: string draft?: Value | undefined current: Value defaultDiffType?: 'deployed' | 'draft' diff --git a/frontend/src/lib/components/raw_apps/RawAppEditor.svelte b/frontend/src/lib/components/raw_apps/RawAppEditor.svelte index cf36d11a62..aec049e8cf 100644 --- a/frontend/src/lib/components/raw_apps/RawAppEditor.svelte +++ b/frontend/src/lib/components/raw_apps/RawAppEditor.svelte @@ -139,7 +139,9 @@ version?: number | undefined /** Moves the draft's base to the deployed head and keeps its content; * offered in the diff drawer while the draft is behind. */ - onTakeLatest?: () => void | Promise + onTakeLatest?: (head?: string) => void | Promise + /** The app_version the draft forked from, threaded to the topbar's diff drawer. */ + draftBaseVersion?: string | undefined // See ScriptBuilderProps — same indicator semantics. loadedFromDraft?: boolean othersDraftsCount?: number @@ -213,7 +215,8 @@ onSavedNewAppPath, condensedHeader = false, version = undefined, - onTakeLatest = undefined + onTakeLatest = undefined, + draftBaseVersion = undefined }: Props = $props() // Workspace this editor operates on: the session's acting workspace when @@ -2309,6 +2312,7 @@ bind:pendingDraftPath {version} {onTakeLatest} + {draftBaseVersion} {onRestore} {onSavedNewAppPath} {policy} diff --git a/frontend/src/lib/components/raw_apps/RawAppEditorHeader.svelte b/frontend/src/lib/components/raw_apps/RawAppEditorHeader.svelte index 8799df9310..0675c2eb8a 100644 --- a/frontend/src/lib/components/raw_apps/RawAppEditorHeader.svelte +++ b/frontend/src/lib/components/raw_apps/RawAppEditorHeader.svelte @@ -111,7 +111,10 @@ version?: number | undefined /** Moves the draft's base to the deployed head and keeps its content; * offered in the diff drawer while the draft is behind. */ - onTakeLatest?: () => void | Promise + onTakeLatest?: (head?: string) => void | Promise + /** The app_version the draft forked from; the drawer offers `onTakeLatest` only + * while it differs from the head on display. */ + draftBaseVersion?: string | undefined newApp: boolean newPath?: string /** Initial labels for the app, threaded from the loaded app data. */ @@ -183,6 +186,7 @@ savedApp = $bindable(undefined), version = $bindable(undefined), onTakeLatest = undefined, + draftBaseVersion = undefined, newApp, newPath = '', labels: initialLabels = undefined, @@ -460,6 +464,7 @@ deployed: deployedValue ?? stripRawAppDiffNoise(savedApp), versions: await deployedVersionOptions(), onTakeLatest, + draftBase: draftBaseVersion, loadVersion: async (id) => { const v = await AppService.getAppByVersion({ workspace: opWorkspace!, id: Number(id) }) // Same normalization as `syncWithDeployed`, so switching versions doesn't diff --git a/frontend/src/lib/components/script_builder.ts b/frontend/src/lib/components/script_builder.ts index cf95dfda5b..c28aad82cc 100644 --- a/frontend/src/lib/components/script_builder.ts +++ b/frontend/src/lib/components/script_builder.ts @@ -36,10 +36,13 @@ export interface ScriptBuilderProps { * stop/restart pair is a no-op on a non-live entry. */ userDraftPath?: string - /** Moves the draft's base to the deployed head and keeps its content; offered - * in the diff drawer while the draft is behind. The route owns it because - * the base lives in a per-kind field of the value. */ - onTakeLatest?: () => void | Promise + /** Moves the draft's base to the head and keeps its content; offered in the diff + * drawer while the draft is behind. Called with the version the drawer shows as + * head. The route owns it because the base lives in a per-kind field of the value. */ + onTakeLatest?: (head?: string) => void | Promise + /** The hash the draft forked from, so the drawer offers `onTakeLatest` only while + * it differs from the head on display. */ + draftBaseHash?: string /** * Workspace + path the AutosaveIndicator watches for sync state. Default * (undefined) falls back to `$workspaceStore` / `userDraftPath` — the diff --git a/frontend/src/routes/(root)/(logged)/apps_raw/edit/[...path]/+page.svelte b/frontend/src/routes/(root)/(logged)/apps_raw/edit/[...path]/+page.svelte index ddf58cda89..c1d512249c 100644 --- a/frontend/src/routes/(root)/(logged)/apps_raw/edit/[...path]/+page.svelte +++ b/frontend/src/routes/(root)/(logged)/apps_raw/edit/[...path]/+page.svelte @@ -625,19 +625,20 @@ newApp={isNewApp} version={parentVersion ?? (deployedHeadVersion != null ? Number(deployedHeadVersion) : undefined)} - onTakeLatest={draftBaseVersion && - deployedHeadVersion && - draftBaseVersion !== deployedHeadVersion - ? async () => { - // Re-read rather than trusting the head this page loaded with; see + {draftBaseVersion} + onTakeLatest={draftBaseVersion + ? async (shown?: string) => { + // The version the drawer showed as head, else a fresh read; see // /scripts/edit. const head = + (shown != null ? Number(shown) : undefined) ?? ( await AppService.getAppLatestVersion({ workspace: $workspaceStore!, path: page.params.path ?? '' }).catch(() => undefined) - )?.version ?? Number(deployedHeadVersion) + )?.version ?? + Number(deployedHeadVersion) parentVersion = head if (deployedBaseline) deployedBaseline = { ...deployedBaseline, parent_version: head } draftBaseVersion = String(head) diff --git a/frontend/src/routes/(root)/(logged)/flows/edit/[...path]/+page.svelte b/frontend/src/routes/(root)/(logged)/flows/edit/[...path]/+page.svelte index 0a7eb66067..5133a70223 100644 --- a/frontend/src/routes/(root)/(logged)/flows/edit/[...path]/+page.svelte +++ b/frontend/src/routes/(root)/(logged)/flows/edit/[...path]/+page.svelte @@ -544,17 +544,18 @@ {:else if renderEditor} { - // Re-read rather than trusting the head this page loaded with; see - // /scripts/edit. + onTakeLatest={draftBaseVersion + ? async (shown?: string) => { + // The version the drawer showed as head, else a fresh read; see /scripts/edit. const head = + (shown != null ? Number(shown) : undefined) ?? ( await FlowService.getFlowLatestVersion({ workspace: $workspaceStore!, path: flowDraftPath }).catch(() => undefined) - )?.id ?? version + )?.id ?? + version if (!draftSync.draft || head == null || !$workspaceStore) return draftSync.draft = { ...draftSync.draft, version_id: head } draftBaseVersion = String(head) diff --git a/frontend/src/routes/(root)/(logged)/scripts/edit/[...path]/+page.svelte b/frontend/src/routes/(root)/(logged)/scripts/edit/[...path]/+page.svelte index e9baaa79aa..214595eb51 100644 --- a/frontend/src/routes/(root)/(logged)/scripts/edit/[...path]/+page.svelte +++ b/frontend/src/routes/(root)/(logged)/scripts/edit/[...path]/+page.svelte @@ -525,18 +525,21 @@ bind:this={scriptBuilder} {initialPath} userDraftPath={draftPath} - onTakeLatest={draftBaseHash && deployedHeadHash && draftBaseHash !== deployedHeadHash - ? async () => { - // Re-read rather than trusting the head this page loaded with: taking a - // stale one would say the draft is up to date with a version that is not - // the latest any more. + {draftBaseHash} + onTakeLatest={draftBaseHash + ? async (shown?: string) => { + // The version the drawer showed as head, else a fresh read: taking the one + // this page loaded with would say the draft is up to date with a version + // that is not the latest any more. const head = + shown ?? ( await ScriptService.getScriptLatestVersion({ workspace: $workspaceStore!, path: draftPath }).catch(() => undefined) - )?.script_hash ?? deployedHeadHash + )?.script_hash ?? + deployedHeadHash if (!draftSync.draft || !head || !$workspaceStore) return draftSync.draft = { ...draftSync.draft, parent_hash: head } // The baseline mirrors the draft's base so an unedited draft still