diff --git a/frontend/src/lib/components/DiffDrawer.svelte b/frontend/src/lib/components/DiffDrawer.svelte index ea94567649..c1655da487 100644 --- a/frontend/src/lib/components/DiffDrawer.svelte +++ b/frontend/src/lib/components/DiffDrawer.svelte @@ -50,6 +50,7 @@ * way to tell what their draft is being compared against. */ deployedLabel?: string versions?: DiffVersionOption[] + onTakeLatest?: () => void | Promise draft: DiffData | undefined current: DiffData path?: string @@ -112,6 +113,18 @@ } } + let takingLatest = $state(false) + async function takeLatest() { + if (!data || data.mode !== 'normal' || !data.onTakeLatest || takingLatest) return + takingLatest = true + try { + await data.onTakeLatest() + diffViewer?.closeDrawer() + } finally { + takingLatest = false + } + } + export function setDiff( diff: | { @@ -120,6 +133,7 @@ deployedLabel?: string versions?: DiffVersionOption[] loadVersion?: (id: string) => Promise + onTakeLatest?: () => void | Promise draft?: Value | undefined current: Value defaultDiffType?: 'deployed' | 'draft' @@ -134,7 +148,16 @@ } ) { if (diff.mode === 'normal') { - const { deployed, deployedLabel, versions, loadVersion, draft, current, button } = diff + const { + deployed, + deployedLabel, + versions, + loadVersion, + onTakeLatest, + draft, + current, + button + } = diff versionLoader = loadVersion headLabel = deployedLabel selectedVersion = versions?.find((v) => v.isHead)?.id @@ -143,6 +166,7 @@ deployed: !deployed.draft_only ? prepareDiff(deployed) : undefined, deployedLabel, versions, + onTakeLatest, draft: draft ? prepareDiff(draft) : undefined, current: prepareDiff(current), path: draft?.path || deployed?.path, @@ -279,6 +303,11 @@ {/if} {#snippet actions()} + {#if data?.mode === 'normal' && data.onTakeLatest} + + {/if} {#if data?.mode === 'normal'} - {#if onTakeLatest} - - {/if} - - + + diff --git a/frontend/src/lib/components/diff_drawer.ts b/frontend/src/lib/components/diff_drawer.ts index 873a26dd41..60a125ec0b 100644 --- a/frontend/src/lib/components/diff_drawer.ts +++ b/frontend/src/lib/components/diff_drawer.ts @@ -27,6 +27,10 @@ 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 draft?: Value | undefined current: Value defaultDiffType?: 'deployed' | 'draft' diff --git a/frontend/src/lib/components/flow_builder.ts b/frontend/src/lib/components/flow_builder.ts index a439a8aba4..f9b8e64aa2 100644 --- a/frontend/src/lib/components/flow_builder.ts +++ b/frontend/src/lib/components/flow_builder.ts @@ -14,6 +14,9 @@ export type FlowBuilderProps = { * draft's `draft_path` so the topbar shows the pending name, so it can't be used * to resolve what is actually deployed. */ 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. */ + onTakeLatest?: () => void | Promise pathStoreInit?: string | undefined newFlow: boolean selectedId: string | undefined diff --git a/frontend/src/lib/components/raw_apps/RawAppEditor.svelte b/frontend/src/lib/components/raw_apps/RawAppEditor.svelte index 7dfb3630a0..5ddb6d1b91 100644 --- a/frontend/src/lib/components/raw_apps/RawAppEditor.svelte +++ b/frontend/src/lib/components/raw_apps/RawAppEditor.svelte @@ -136,6 +136,9 @@ * deployed" guard: deploying is refused with a confirmation while it is not * the head. Undefined for a draft-only app. */ 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 // See ScriptBuilderProps — same indicator semantics. loadedFromDraft?: boolean othersDraftsCount?: number @@ -208,7 +211,8 @@ onRestore, onSavedNewAppPath, condensedHeader = false, - version = undefined + version = undefined, + onTakeLatest = undefined }: Props = $props() // Workspace this editor operates on: the session's acting workspace when @@ -2303,6 +2307,7 @@ bind:summary bind:pendingDraftPath {version} + {onTakeLatest} {onRestore} {onSavedNewAppPath} {policy} diff --git a/frontend/src/lib/components/raw_apps/RawAppEditorHeader.svelte b/frontend/src/lib/components/raw_apps/RawAppEditorHeader.svelte index e1c38b88ac..a865b46cdc 100644 --- a/frontend/src/lib/components/raw_apps/RawAppEditorHeader.svelte +++ b/frontend/src/lib/components/raw_apps/RawAppEditorHeader.svelte @@ -109,6 +109,9 @@ } | undefined 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 newApp: boolean newPath?: string /** Initial labels for the app, threaded from the loaded app data. */ @@ -179,6 +182,7 @@ diffDrawer = undefined, savedApp = $bindable(undefined), version = $bindable(undefined), + onTakeLatest = undefined, newApp, newPath = '', labels: initialLabels = undefined, @@ -445,6 +449,7 @@ mode: 'normal', deployed: deployedValue ?? stripRawAppDiffNoise(savedApp), versions: await deployedVersionOptions(), + onTakeLatest, 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 69916f321b..cf95dfda5b 100644 --- a/frontend/src/lib/components/script_builder.ts +++ b/frontend/src/lib/components/script_builder.ts @@ -36,6 +36,10 @@ 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 /** * 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/edit/[...path]/+page.svelte b/frontend/src/routes/(root)/(logged)/apps/edit/[...path]/+page.svelte index 1577cf1c21..d38f45eb8e 100644 --- a/frontend/src/routes/(root)/(logged)/apps/edit/[...path]/+page.svelte +++ b/frontend/src/routes/(root)/(logged)/apps/edit/[...path]/+page.svelte @@ -465,13 +465,6 @@ {draftBaseVersion} {deployedHeadVersion} {deployedBy} - onTakeLatest={async () => { - const head = deployedHeadVersion != null ? Number(deployedHeadVersion) : undefined - if (!app?.value || head == null || !$workspaceStore) return - ;(app.value as App).parent_version = head - draftBaseVersion = String(head) - await UserDraft.forcePersist('app', path, { workspace: $workspaceStore }) - }} onLoadLatestDeploy={async () => { if (!$workspaceStore) return await runResetToDeployed({ 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 87717b405d..0eadf0746b 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 @@ -582,14 +582,6 @@ {draftBaseVersion} {deployedHeadVersion} {deployedBy} - onTakeLatest={() => { - const head = deployedHeadVersion != null ? Number(deployedHeadVersion) : undefined - if (head == null) return - // The bundle carries `parentVersion`, so this alone re-persists the draft. - parentVersion = head - if (deployedBaseline) deployedBaseline = { ...deployedBaseline, parent_version: head } - draftBaseVersion = String(head) - }} onViewDiff={() => rawAppEditor?.openDiffDrawer()} onLoadLatestDeploy={async () => { // stopSync-bracketed; see /scripts/edit's restoreDeployed for the race. @@ -630,6 +622,17 @@ {diffDrawer} newApp={isNewApp} version={parentVersion} + onTakeLatest={draftBaseVersion && + deployedHeadVersion && + draftBaseVersion !== deployedHeadVersion + ? () => { + const head = Number(deployedHeadVersion) + // The bundle carries `parentVersion`, so this alone re-persists the draft. + parentVersion = head + if (deployedBaseline) deployedBaseline = { ...deployedBaseline, parent_version: head } + draftBaseVersion = String(head) + } + : undefined} onDeploy={({ version }) => { // The version just written is the new head, and the base the next // autosave should carry. 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 8fc466af2f..2dcb3fe57a 100644 --- a/frontend/src/routes/(root)/(logged)/flows/edit/[...path]/+page.svelte +++ b/frontend/src/routes/(root)/(logged)/flows/edit/[...path]/+page.svelte @@ -528,13 +528,6 @@ {draftBaseVersion} deployedHeadVersion={version != null ? String(version) : undefined} {deployedBy} - onTakeLatest={async () => { - const head = version - if (!draftSync.draft || head == null || !$workspaceStore) return - draftSync.draft = { ...draftSync.draft, version_id: head } - draftBaseVersion = String(head) - await UserDraft.forcePersist('flow', flowDraftPath, { workspace: $workspaceStore }) - }} onViewDiff={() => flowBuilder?.openDiffDrawer()} onBeforeRelocate={() => flowBuilder?.saveDraft()} onLoadLatestDeploy={async () => { @@ -558,6 +551,15 @@ {:else if renderEditor} { + const head = version + if (!draftSync.draft || head == null || !$workspaceStore) return + draftSync.draft = { ...draftSync.draft, version_id: head } + draftBaseVersion = String(head) + await UserDraft.forcePersist('flow', flowDraftPath, { workspace: $workspaceStore }) + } + : undefined} onDeploy={(e) => { // stopSync-bracketed immediate delete; see /scripts/edit's restoreDeployed. if ($workspaceStore) { 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 4708390cdf..37495e5c34 100644 --- a/frontend/src/routes/(root)/(logged)/scripts/edit/[...path]/+page.svelte +++ b/frontend/src/routes/(root)/(logged)/scripts/edit/[...path]/+page.svelte @@ -489,16 +489,6 @@ draftBaseVersion={draftBaseHash} deployedHeadVersion={deployedHeadHash} {deployedBy} - onTakeLatest={async () => { - const head = 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 - // compares equal and the autosave can discard it. - if (deployedBaseline) deployedBaseline = { ...deployedBaseline, parent_hash: head } - draftBaseHash = head - await UserDraft.forcePersist('script', draftPath, { workspace: $workspaceStore }) - }} onViewDiff={() => scriptBuilder?.openDiffDrawer()} onBeforeRelocate={() => scriptBuilder?.saveDraft()} onLoadLatestDeploy={async () => { @@ -520,6 +510,18 @@ bind:this={scriptBuilder} {initialPath} userDraftPath={draftPath} + onTakeLatest={draftBaseHash && deployedHeadHash && draftBaseHash !== deployedHeadHash + ? async () => { + const head = 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 + // compares equal and the autosave can discard it. + if (deployedBaseline) deployedBaseline = { ...deployedBaseline, parent_hash: head } + draftBaseHash = head + await UserDraft.forcePersist('script', draftPath, { workspace: $workspaceStore }) + } + : undefined} bind:script={draftSync.draft} template={builderTemplate} {lockedLanguage}