feat: take latest adopts the head the diff shows, and is offered while the drawer sees the draft behind

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Ruben Fiszel
2026-09-15 09:02:51 +02:00
co-authored by Claude Opus 5
parent 194c3218d8
commit 062adc284b
10 changed files with 72 additions and 32 deletions
+20 -4
View File
@@ -51,7 +51,8 @@
* way to tell what their draft is being compared against. */
deployedLabel?: string
versions?: DiffVersionOption[]
onTakeLatest?: () => void | Promise<void>
onTakeLatest?: (head?: string) => void | Promise<void>
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<Value | undefined>
onTakeLatest?: () => void | Promise<void>
onTakeLatest?: (head?: string) => void | Promise<void>
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}
</div>
{#snippet actions()}
{#if data?.mode === 'normal' && data.onTakeLatest}
{#if data?.mode === 'normal' && data.onTakeLatest && behindShown}
<Button unifiedSize="sm" variant="default" loading={takingLatest} onClick={takeLatest}>
Take latest, keep my edits
</Button>
@@ -1175,6 +1175,7 @@
deployedLabel,
versions: await deployedVersionOptions(),
onTakeLatest,
draftBase: draftBaseVersion,
loadVersion: async (id) => {
const v = await FlowService.getFlowVersion({
workspace: opWorkspace!,
@@ -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({
+8 -4
View File
@@ -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<Value | undefined>
/** 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<void>
/** 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<void>
/** 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'
@@ -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<void>
onTakeLatest?: (head?: string) => void | Promise<void>
/** 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}
@@ -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<void>
onTakeLatest?: (head?: string) => void | Promise<void>
/** 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
@@ -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<void>
/** 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<void>
/** 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
@@ -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)
@@ -544,17 +544,18 @@
</div>
{:else if renderEditor}
<FlowBuilder
onTakeLatest={draftBaseVersion && version != null && draftBaseVersion !== String(version)
? async () => {
// 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)
@@ -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