mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-09-21 00:02:30 +00:00
feat: raw-app drafts carry a fork base, so behind means base != head for them too
The raw-app bundle never carried the version it forked from, which left raw apps on the timestamp check that self-heals as you type, and the header's deploy guard read a version prop nothing set, so deploying over a newer version never asked. The route now stamps parent_version into the bundle (the draft's own base when it has one, else the head), the server derives draft.base from it, the stale prompt compares it to the head and links to the diff, and the editor threads it to the header so the deploy guard confirms. A deploy re-pins the base to the version it wrote. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5.1
parent
7ad7312cd0
commit
5de97332d3
@@ -102,8 +102,9 @@
|
||||
| undefined
|
||||
diffDrawer?: DiffDrawer | undefined
|
||||
onNavigate?: (item: import('$lib/components/workspacePicker').WorkspaceItem) => void
|
||||
/** Fired after a successful deploy; the session preview reloads on it. */
|
||||
onDeploy?: (e: { path: string }) => void
|
||||
/** Fired after a successful deploy; the session preview reloads on it and
|
||||
* the route re-pins the draft's fork base to the version just written. */
|
||||
onDeploy?: (e: { path: string; version?: number }) => void
|
||||
/** Initial collapsed state for the file/runnable sidebar. The user's
|
||||
* toggled preference is persisted under `sidebarStorageKey`; this prop
|
||||
* only seeds the very first open. */
|
||||
@@ -131,6 +132,10 @@
|
||||
pendingDraftPath?: string | undefined
|
||||
// Threaded to the AutosaveIndicator's "Reset to deployed" button.
|
||||
onResetToDeployed?: () => void | Promise<void>
|
||||
/** The app_version the draft forked from, for the deploy-time "new version
|
||||
* deployed" guard: deploying is refused with a confirmation while it is not
|
||||
* the head. Undefined for a draft-only app. */
|
||||
version?: number | undefined
|
||||
// See ScriptBuilderProps — same indicator semantics.
|
||||
loadedFromDraft?: boolean
|
||||
othersDraftsCount?: number
|
||||
@@ -202,9 +207,9 @@
|
||||
onScreenshotRequester = undefined,
|
||||
onRestore,
|
||||
onSavedNewAppPath,
|
||||
condensedHeader = false
|
||||
condensedHeader = false,
|
||||
version = undefined
|
||||
}: Props = $props()
|
||||
export const version: number | undefined = undefined
|
||||
|
||||
// Workspace this editor operates on: the session's acting workspace when
|
||||
// embedded in a session preview (autosaveWorkspace), else the navigation
|
||||
@@ -248,6 +253,12 @@
|
||||
// so the preview just opens the app.
|
||||
setOpenInSessionHandoff({ source: () => sessionOpen })
|
||||
|
||||
let header: RawAppEditorHeader | undefined = $state(undefined)
|
||||
/** The Deployed↔Current diff, for the route's stale-draft prompt. */
|
||||
export function openDiffDrawer() {
|
||||
return header?.openDiffDrawer()
|
||||
}
|
||||
|
||||
/** Hand this app off to a fresh AI session, seeding `seedPrompt` and sending
|
||||
* it on arrival. Exposed for the template picker's "Start in AI session": the
|
||||
* route owns the prompt, but the draft persistence the preview depends on
|
||||
@@ -2285,11 +2296,13 @@
|
||||
/>
|
||||
<div bind:clientWidth={rootWidth} class="max-h-full overflow-hidden h-full min-h-0 flex flex-col">
|
||||
<RawAppEditorHeader
|
||||
bind:this={header}
|
||||
bind:jobs
|
||||
bind:jobsById
|
||||
bind:savedApp
|
||||
bind:summary
|
||||
bind:pendingDraftPath
|
||||
{version}
|
||||
{onRestore}
|
||||
{onSavedNewAppPath}
|
||||
{policy}
|
||||
|
||||
@@ -146,8 +146,9 @@
|
||||
* falls back to `$workspaceStore`/`liveEditorDraftStoragePath`. */
|
||||
autosaveWorkspace?: string
|
||||
autosavePath?: string
|
||||
// Fired after a successful deploy; lets the session preview reload.
|
||||
onDeploy?: (e: { path: string }) => void
|
||||
// Fired after a successful deploy; lets the session preview reload. Carries
|
||||
// the version just written so the route can re-pin the draft's fork base.
|
||||
onDeploy?: (e: { path: string; version?: number }) => void
|
||||
/** Surfaces the user-typed path (`newEditedPath`) up to the route
|
||||
* when (and only when) it differs from the deployed/seeded
|
||||
* `savedApp.path`. The route writes it into the autosaved raw-app
|
||||
@@ -431,7 +432,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
async function openDiffDrawer() {
|
||||
export async function openDiffDrawer() {
|
||||
if (!savedApp) {
|
||||
return
|
||||
}
|
||||
@@ -513,7 +514,7 @@
|
||||
if (appPath !== npath) {
|
||||
onSavedNewAppPath?.(npath)
|
||||
}
|
||||
onDeploy?.({ path: npath })
|
||||
onDeploy?.({ path: npath, version })
|
||||
}
|
||||
|
||||
async function setPublishState(message?: string) {
|
||||
|
||||
@@ -16,6 +16,9 @@ export type RawAppDraft = {
|
||||
// friendly name (they read `value->>'draft_path'`) — and so editing the path
|
||||
// in the editor changes the persisted draft and triggers an autosave.
|
||||
draft_path?: string
|
||||
// The app_version the draft forked from. The server derives `draft.base` from
|
||||
// it, which is what tells a draft that is behind the deployed head.
|
||||
parent_version?: number
|
||||
}
|
||||
|
||||
// The shape a raw-app cell's store (`RawAppRuntimeValue` in
|
||||
@@ -30,6 +33,7 @@ export type RuntimeRawApp = {
|
||||
policy: any
|
||||
custom_path?: string
|
||||
draft_path?: string
|
||||
parent_version?: number
|
||||
}
|
||||
|
||||
// Strip runtime-only metadata (just `path`, the storage key) when persisting
|
||||
@@ -43,7 +47,8 @@ export function runtimeRawAppToDraft(raw: RuntimeRawApp): RawAppDraft {
|
||||
data: raw.data,
|
||||
policy: raw.policy,
|
||||
custom_path: raw.custom_path,
|
||||
draft_path: raw.draft_path
|
||||
draft_path: raw.draft_path,
|
||||
parent_version: raw.parent_version
|
||||
}
|
||||
}
|
||||
|
||||
@@ -58,6 +63,7 @@ export function applyDraftToRuntimeRawApp(raw: RuntimeRawApp, dv: RawAppDraft):
|
||||
data: dv.data,
|
||||
policy: dv.policy ?? raw.policy,
|
||||
custom_path: dv.custom_path ?? raw.custom_path,
|
||||
draft_path: dv.draft_path ?? raw.draft_path
|
||||
draft_path: dv.draft_path ?? raw.draft_path,
|
||||
parent_version: dv.parent_version ?? raw.parent_version
|
||||
}
|
||||
}
|
||||
|
||||
@@ -122,6 +122,7 @@
|
||||
summary,
|
||||
policy,
|
||||
custom_path: savedApp?.custom_path,
|
||||
parent_version: parentVersion,
|
||||
// Persist the typed path as `draft_path` only when it actually differs
|
||||
// from the current path — a `draft_path` equal to the baseline is a
|
||||
// no-op that would block the draft from deduping against the deployed
|
||||
@@ -165,6 +166,13 @@
|
||||
let othersModalOpen = $state(false)
|
||||
let draftSavedAt = $state<string | undefined>(undefined)
|
||||
let deployedAt = $state<string | undefined>(undefined)
|
||||
// The app_version the draft forked from, and the deployed head, for the exact
|
||||
// staleness check (vs the drifting timestamps). `parentVersion` is what the
|
||||
// bundle carries: an own draft keeps the version it forked from; a fresh
|
||||
// checkout forks from the head.
|
||||
let parentVersion = $state<number | undefined>(undefined)
|
||||
let draftBaseVersion = $state<string | undefined>(undefined)
|
||||
let deployedHeadVersion = $state<string | undefined>(undefined)
|
||||
async function loadApp(opts: { getDraft?: boolean } = {}): Promise<void> {
|
||||
const getDraft = opts.getDraft ?? true
|
||||
const tok = ++loadAppToken
|
||||
@@ -185,6 +193,9 @@
|
||||
loadedFromDraft = false
|
||||
draftSavedAt = undefined
|
||||
deployedAt = undefined
|
||||
parentVersion = undefined
|
||||
draftBaseVersion = undefined
|
||||
deployedHeadVersion = undefined
|
||||
// `labels` is route-level state; reset it too so a fresh draft doesn't
|
||||
// inherit (and then deploy) the previously-opened app's labels. The
|
||||
// import branch re-seeds it via extractRawApp below.
|
||||
@@ -301,6 +312,15 @@
|
||||
// See /apps/edit's loader.
|
||||
draftSavedAt = backendApp.draft_saved_at as string | undefined
|
||||
deployedAt = backendApp.no_deployed ? undefined : (backendApp.created_at as string | undefined)
|
||||
// Head = the last entry of the deployed `versions`. The base the bundle
|
||||
// carries is the draft's own when it has one; a draft that predates the
|
||||
// base (or a fresh checkout) forks from the head from here on.
|
||||
const versions = backendApp.versions as number[] | undefined
|
||||
const headVersion =
|
||||
backendApp.no_deployed || !Array.isArray(versions) ? undefined : versions[versions.length - 1]
|
||||
deployedHeadVersion = headVersion != null ? String(headVersion) : undefined
|
||||
draftBaseVersion = backendApp.draft_base as string | undefined
|
||||
parentVersion = hasOwnDraft && draftBaseVersion != null ? Number(draftBaseVersion) : headVersion
|
||||
// Deployed baseline for the autosave `discardIf`, captured BEFORE the swap
|
||||
// below mutates `backendApp`. Mirrors the bundle `$effect`'s shape (minus
|
||||
// the edit-only `draft_path`) so an unedited draft compares equal.
|
||||
@@ -314,7 +334,8 @@
|
||||
data: extractDataConfig(backendApp.value) ?? { ...DEFAULT_DATA },
|
||||
summary: backendApp.summary ?? '',
|
||||
policy: backendApp.policy,
|
||||
custom_path: backendApp.custom_path
|
||||
custom_path: backendApp.custom_path,
|
||||
parent_version: parentVersion
|
||||
})
|
||||
) as RawAppDraft)
|
||||
// The raw-app autosave stores a flat `RawAppDraft`, but this loader (and
|
||||
@@ -549,6 +570,9 @@
|
||||
bind:othersModalOpen
|
||||
{draftSavedAt}
|
||||
{deployedAt}
|
||||
{draftBaseVersion}
|
||||
{deployedHeadVersion}
|
||||
onViewDiff={() => rawAppEditor?.openDiffDrawer()}
|
||||
onLoadLatestDeploy={async () => {
|
||||
// stopSync-bracketed; see /scripts/edit's restoreDeployed for the race.
|
||||
if (!$workspaceStore) return
|
||||
@@ -587,6 +611,15 @@
|
||||
bind:savedApp
|
||||
{diffDrawer}
|
||||
newApp={isNewApp}
|
||||
version={parentVersion}
|
||||
onDeploy={({ version }) => {
|
||||
// The version just written is the new head, and the base the next
|
||||
// autosave should carry.
|
||||
if (version != null) {
|
||||
parentVersion = version
|
||||
deployedHeadVersion = String(version)
|
||||
}
|
||||
}}
|
||||
onResetToDeployed={reloadDeployed}
|
||||
{loadedFromDraft}
|
||||
othersDraftsCount={otherDraftsUsers.length}
|
||||
|
||||
Reference in New Issue
Block a user