mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-09-21 00:02:30 +00:00
feat: store the version a draft forked from in one draft.base column
Every kind kept its fork base under a different name and type inside the value: parent_hash (hex) for scripts, version_id for flows, parent_version for apps. draft.base holds it as one text id, derived on save from the value so every writer fills it the same way, backfilled by the migration (rows holding a NUL are skipped, since ->> raises on them). The get-by-path overlay exposes it as draft_base and the drafts list as base; the editors and the compare page read that one field and compare it to the head as text. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5.1
parent
57d0756160
commit
7ad7312cd0
+9
-3
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT value as \"value!: sqlx::types::Json<Box<serde_json::value::RawValue>>\",\n created_at\n FROM draft\n WHERE workspace_id = $1\n AND (email = $2 OR email IS NULL)\n AND path = $3\n AND typ = $4\n ORDER BY email NULLS LAST\n LIMIT 1",
|
||||
"query": "SELECT value as \"value!: sqlx::types::Json<Box<serde_json::value::RawValue>>\",\n created_at, base\n FROM draft\n WHERE workspace_id = $1\n AND (email = $2 OR email IS NULL)\n AND path = $3\n AND typ = $4\n ORDER BY email NULLS LAST\n LIMIT 1",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
@@ -12,6 +12,11 @@
|
||||
"ordinal": 1,
|
||||
"name": "created_at",
|
||||
"type_info": "Timestamptz"
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "base",
|
||||
"type_info": "Text"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
@@ -58,8 +63,9 @@
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false
|
||||
false,
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "d4e0da9f9653d532770066310f85e59e5edda1faea72f39603814afb6a3cd596"
|
||||
"hash": "0cc6770a81ecaecafe0d9b7100f94b329c4ec7978b016af4c964415e0c38396c"
|
||||
}
|
||||
+4
-3
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "INSERT INTO draft (workspace_id, email, path, typ, value, created_at)\n VALUES ($1, $2, $3, $4, $5::text::json, COALESCE($8::timestamptz, now()))\n ON CONFLICT (workspace_id, path, typ, email) WHERE email IS NOT NULL\n DO UPDATE SET value = EXCLUDED.value, created_at = EXCLUDED.created_at\n WHERE $7::bool = true\n OR $6::timestamptz IS NULL\n OR draft.created_at <= $6::timestamptz\n RETURNING created_at",
|
||||
"query": "INSERT INTO draft (workspace_id, email, path, typ, value, created_at, base)\n VALUES ($1, $2, $3, $4, $5::text::json, COALESCE($8::timestamptz, now()), $9)\n ON CONFLICT (workspace_id, path, typ, email) WHERE email IS NOT NULL\n DO UPDATE SET value = EXCLUDED.value, created_at = EXCLUDED.created_at,\n base = EXCLUDED.base\n WHERE $7::bool = true\n OR $6::timestamptz IS NULL\n OR draft.created_at <= $6::timestamptz\n RETURNING created_at",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
@@ -52,12 +52,13 @@
|
||||
"Text",
|
||||
"Timestamptz",
|
||||
"Bool",
|
||||
"Timestamptz"
|
||||
"Timestamptz",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "c8fb2a1491f90951a6d2e4e9c56d288eb60f6c6644a73cdf949de0159215a7f7"
|
||||
"hash": "d3686c73788b866090f25383da507e9a7d9f90d5f86fa4a1b89026cb088d0a58"
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
ALTER TABLE draft DROP COLUMN base;
|
||||
@@ -0,0 +1,14 @@
|
||||
-- The version a draft forked from, as one opaque text id whatever the kind: a
|
||||
-- script hash (hex), a flow_version.id, an app_version.id. NULL for a draft that
|
||||
-- was never forked from a deploy and for kinds that keep no lineage.
|
||||
ALTER TABLE draft ADD COLUMN base TEXT;
|
||||
|
||||
-- A U+0000 inside a `json` value makes `->>` raise 22P05; such rows keep NULL and
|
||||
-- get their base on their next save.
|
||||
UPDATE draft SET base = CASE typ::text
|
||||
WHEN 'script' THEN value ->> 'parent_hash'
|
||||
WHEN 'flow' THEN value ->> 'version_id'
|
||||
ELSE value ->> 'parent_version'
|
||||
END
|
||||
WHERE typ::text IN ('script', 'flow', 'app', 'raw_app')
|
||||
AND position(chr(92) || 'u0000' in replace(value::text, chr(92) || chr(92), '')) = 0;
|
||||
@@ -9301,6 +9301,9 @@ paths:
|
||||
created_at:
|
||||
type: string
|
||||
format: date-time
|
||||
base:
|
||||
type: string
|
||||
description: The deployed version this draft forked from (script hash, flow version id or app version id, as text). Absent when the draft was never forked from a deploy.
|
||||
can_write:
|
||||
type: boolean
|
||||
description: Whether the current user may deploy/discard this draft (same check the deploy/discard endpoints enforce).
|
||||
@@ -26102,6 +26105,13 @@ components:
|
||||
draft_saved_at:
|
||||
type: string
|
||||
format: date-time
|
||||
draft_base:
|
||||
type: string
|
||||
description: |
|
||||
The deployed version the draft forked from, as text whatever the
|
||||
kind (script hash, flow version id, app version id). Compare to the
|
||||
deployed head to tell a draft that is behind. Absent when there is
|
||||
no draft or it was never forked from a deploy.
|
||||
no_deployed:
|
||||
type: boolean
|
||||
draft:
|
||||
|
||||
@@ -57,6 +57,11 @@ pub struct DraftListItem {
|
||||
/// row exists at this (path, kind) — the DISTINCT ON prefers an owned row.
|
||||
pub legacy_draft: bool,
|
||||
pub created_at: chrono::DateTime<chrono::Utc>,
|
||||
/// The deployed version this draft forked from (`draft.base`): a script
|
||||
/// hash, a flow version id or an app version id, always as text. `None` for
|
||||
/// a draft that was never forked from a deploy.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub base: Option<String>,
|
||||
/// All draft authors at this `(path, kind)`, for the shared full-page-editor
|
||||
/// kinds (script/flow/app/raw_app) only — feeds the home-page-style owner
|
||||
/// circles on the review page. `None` for drawer kinds, which keep their
|
||||
@@ -245,6 +250,7 @@ fn list_drafts_query(all_users: bool) -> String {
|
||||
d.path,
|
||||
d.typ AS kind,
|
||||
d.created_at,
|
||||
d.base,
|
||||
d.value ->> 'summary' AS summary,
|
||||
{draft_users} AS draft_users,
|
||||
-- Friendly typed path, by kind (mirrors the home-page list
|
||||
@@ -357,6 +363,20 @@ struct DraftBaseVersion {
|
||||
parent_version: Option<i64>,
|
||||
}
|
||||
|
||||
impl DraftBaseVersion {
|
||||
/// The base as the `draft.base` column stores it: one opaque text id whatever
|
||||
/// the kind, so a reader compares it to the head without knowing the kind's
|
||||
/// own field name or type.
|
||||
fn as_text(&self, kind: UserDraftItemKind) -> Option<String> {
|
||||
use UserDraftItemKind::*;
|
||||
match kind {
|
||||
Script => self.parent_hash.clone(),
|
||||
Flow => self.version_id.map(|v| v.to_string()),
|
||||
_ => self.parent_version.map(|v| v.to_string()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The version this draft forked from, or `None` when it has no lineage to
|
||||
/// follow — a kind that keeps none, a malformed payload, or a draft that was
|
||||
/// never forked from a deploy. Pure: no queries. This is the escape hatch the
|
||||
@@ -705,11 +725,15 @@ async fn update_draft(
|
||||
Some(_) => None,
|
||||
None => Some(db.acquire().await?),
|
||||
};
|
||||
// `base` is derived here from the value's per-kind field rather than sent
|
||||
// by the client, so every writer (editors, chat, CLI) fills it the same way.
|
||||
let base = lineage.as_ref().and_then(|l| l.as_text(kind));
|
||||
let applied = sqlx::query_scalar!(
|
||||
r#"INSERT INTO draft (workspace_id, email, path, typ, value, created_at)
|
||||
VALUES ($1, $2, $3, $4, $5::text::json, COALESCE($8::timestamptz, now()))
|
||||
r#"INSERT INTO draft (workspace_id, email, path, typ, value, created_at, base)
|
||||
VALUES ($1, $2, $3, $4, $5::text::json, COALESCE($8::timestamptz, now()), $9)
|
||||
ON CONFLICT (workspace_id, path, typ, email) WHERE email IS NOT NULL
|
||||
DO UPDATE SET value = EXCLUDED.value, created_at = EXCLUDED.created_at
|
||||
DO UPDATE SET value = EXCLUDED.value, created_at = EXCLUDED.created_at,
|
||||
base = EXCLUDED.base
|
||||
WHERE $7::bool = true
|
||||
OR $6::timestamptz IS NULL
|
||||
OR draft.created_at <= $6::timestamptz
|
||||
@@ -722,6 +746,7 @@ async fn update_draft(
|
||||
req.last_sync,
|
||||
req.force,
|
||||
req.created_at,
|
||||
base.as_deref(),
|
||||
)
|
||||
.fetch_optional(match (tx.as_mut(), plain.as_mut()) {
|
||||
(Some(tx), _) => &mut **tx as &mut sqlx::PgConnection,
|
||||
|
||||
@@ -188,7 +188,6 @@ impl UserDraftItemKind {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// Whether OTHER users' drafts at a path are visible to a viewer (the
|
||||
/// "others are editing" list, owner circles, and the `get_draft_for_user`
|
||||
/// View JSON / Fork endpoint). Enabled only for the full-page editor items
|
||||
@@ -241,6 +240,12 @@ pub struct WithDraftOverlay {
|
||||
pub is_draft: bool,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub draft_saved_at: Option<DateTime<Utc>>,
|
||||
/// The deployed version the draft forked from (`draft.base`), as text
|
||||
/// whatever the kind. The editor compares it to the head it loaded to tell
|
||||
/// a draft that is behind. Absent when there is no draft or it was never
|
||||
/// forked from a deploy.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub draft_base: Option<String>,
|
||||
/// True when no deployed row exists at this path: `inner` is only a
|
||||
/// best-effort stand-in synthesized from the draft and only `draft` is
|
||||
/// canonical. Frontend uses this to disable "diff/reset vs deployed" and
|
||||
@@ -326,6 +331,7 @@ where
|
||||
inner: Box::new(deployed),
|
||||
is_draft: false,
|
||||
draft_saved_at: None,
|
||||
draft_base: None,
|
||||
no_deployed: false,
|
||||
draft: None,
|
||||
other_drafts_users: Vec::new(),
|
||||
@@ -346,7 +352,7 @@ where
|
||||
// row when an owned one exists.
|
||||
let row = sqlx::query!(
|
||||
r#"SELECT value as "value!: sqlx::types::Json<Box<serde_json::value::RawValue>>",
|
||||
created_at
|
||||
created_at, base
|
||||
FROM draft
|
||||
WHERE workspace_id = $1
|
||||
AND (email = $2 OR email IS NULL)
|
||||
@@ -367,6 +373,7 @@ where
|
||||
inner: Box::new(deployed),
|
||||
is_draft: false,
|
||||
draft_saved_at: None,
|
||||
draft_base: None,
|
||||
no_deployed: false,
|
||||
draft: None,
|
||||
other_drafts_users,
|
||||
@@ -379,6 +386,7 @@ where
|
||||
inner: Box::new(deployed),
|
||||
is_draft: true,
|
||||
draft_saved_at: Some(row.created_at),
|
||||
draft_base: row.base,
|
||||
no_deployed: false,
|
||||
draft: Some(draft_json),
|
||||
other_drafts_users,
|
||||
@@ -676,7 +684,7 @@ pub async fn fetch_draft_only(
|
||||
// Own draft first, legacy NULL-email row as fallback (see `maybe_overlay_draft`).
|
||||
let row = sqlx::query!(
|
||||
r#"SELECT value as "value!: sqlx::types::Json<Box<serde_json::value::RawValue>>",
|
||||
created_at
|
||||
created_at, base
|
||||
FROM draft
|
||||
WHERE workspace_id = $1
|
||||
AND (email = $2 OR email IS NULL)
|
||||
@@ -707,6 +715,7 @@ pub async fn fetch_draft_only(
|
||||
inner: Box::new(draft_json.clone()),
|
||||
is_draft: true,
|
||||
draft_saved_at: Some(row.created_at),
|
||||
draft_base: row.base,
|
||||
no_deployed: true,
|
||||
draft: Some(draft_json),
|
||||
other_drafts_users,
|
||||
|
||||
@@ -292,7 +292,7 @@
|
||||
// a draft, else the load-time head. This catches both a concurrent deploy
|
||||
// (head moved since open) AND a stale draft reopened after a deploy (head ==
|
||||
// load-time head, but the draft was forked from an older version).
|
||||
const base = draftBaseVersion ?? version
|
||||
const base = draftBaseVersion ?? (version != null ? String(version) : undefined)
|
||||
if (base === undefined) {
|
||||
return
|
||||
}
|
||||
@@ -303,7 +303,7 @@
|
||||
path: initialPath
|
||||
})
|
||||
|
||||
onLatest = base === flowVersion?.id
|
||||
onLatest = flowVersion != null && base === String(flowVersion.id)
|
||||
} else {
|
||||
onLatest = true
|
||||
}
|
||||
|
||||
@@ -40,15 +40,14 @@
|
||||
draftSavedAt?: string | undefined
|
||||
/** ISO timestamp of the latest deploy at this path. */
|
||||
deployedAt?: string | undefined
|
||||
/** Precise staleness inputs: the deployed version the draft is pinned to, and
|
||||
* the current deployed head. When both are set they drive `isStale` and the
|
||||
* dedup key instead of the timestamps, which drift past `deployedAt` as you
|
||||
* keep editing. Flows/apps pass version ids and pin the true fork point;
|
||||
* scripts pass hashes and re-pin to the head on each load, so one edit after
|
||||
* a dismissed stale prompt stops it recurring — the same self-healing the
|
||||
* timestamps had. Absent (pre-feature drafts) ⇒ timestamp fallback. */
|
||||
draftBaseVersion?: number | string | undefined
|
||||
deployedHeadVersion?: number | string | undefined
|
||||
/** Precise staleness inputs: the deployed version the draft forked from
|
||||
* (`draft_base` on the get-by-path response) and the current deployed head,
|
||||
* both as text whatever the kind. When both are set they drive `isStale` and
|
||||
* the dedup key instead of the timestamps, which drift past `deployedAt` as
|
||||
* you keep editing. Absent (a draft never forked from a deploy) ⇒ timestamp
|
||||
* fallback. */
|
||||
draftBaseVersion?: string | undefined
|
||||
deployedHeadVersion?: string | undefined
|
||||
/** Discard the draft and reload deployed (same as "Reset to deployed"). */
|
||||
onLoadLatestDeploy?: () => void | Promise<void>
|
||||
/** Opens the editor's Deployed↔Current diff from the stale prompt, so the
|
||||
@@ -85,9 +84,7 @@
|
||||
|
||||
// Prefer the version comparison over the timestamp for every kind that supplies
|
||||
// one: `draftSavedAt` advances past `deployedAt` as you keep editing, hiding the
|
||||
// staleness outright. What the version pins differs — flows/apps hold the fork
|
||||
// point, scripts the head of their last load — so see `draftBaseVersion` above
|
||||
// before reasoning about when this stops firing.
|
||||
// staleness outright.
|
||||
const useVersion = $derived(draftBaseVersion != null && deployedHeadVersion != null)
|
||||
const isStale = $derived(
|
||||
!!onLoadLatestDeploy &&
|
||||
|
||||
@@ -28,9 +28,10 @@ export type FlowBuilderProps = {
|
||||
disabledFlowInputs?: boolean
|
||||
savedPrimarySchedule?: ScheduleTrigger | undefined // used to set the primary schedule in the legacy primaryScheduleStore
|
||||
version?: number | undefined
|
||||
/** flow_version the draft was forked from; when set, the deploy-time staleness
|
||||
* check compares it (not the load-time head `version`) against the latest. */
|
||||
draftBaseVersion?: number | undefined
|
||||
/** flow_version the draft was forked from (as text, like `draft_base`); when
|
||||
* set, the deploy-time staleness check compares it (not the load-time head
|
||||
* `version`) against the latest. */
|
||||
draftBaseVersion?: string | undefined
|
||||
draftTriggersFromUrl?: Trigger[] | undefined
|
||||
selectedTriggerIndexFromUrl?: number | undefined
|
||||
children?: import('svelte').Snippet
|
||||
|
||||
@@ -38,30 +38,26 @@ import { ScriptService, FlowService } from '$lib/gen'
|
||||
// fabricates) the "started from an older deployed version" warning.
|
||||
|
||||
describe('draftBaseIsStale', () => {
|
||||
it('script: stale iff the draft parent_hash differs from the deployed hash', () => {
|
||||
expect(draftBaseIsStale('script', { hash: 'v2', draft: { parent_hash: 'v1' } })).toBe(true)
|
||||
expect(draftBaseIsStale('script', { hash: 'v2', draft: { parent_hash: 'v2' } })).toBe(false)
|
||||
it('script: stale iff draft_base differs from the deployed hash', () => {
|
||||
expect(draftBaseIsStale('script', { hash: 'v2', draft_base: 'v1' })).toBe(true)
|
||||
expect(draftBaseIsStale('script', { hash: 'v2', draft_base: 'v2' })).toBe(false)
|
||||
})
|
||||
|
||||
it('script: no base pointer or no head → not stale (nothing to compare)', () => {
|
||||
it('script: no base or no head → not stale (nothing to compare)', () => {
|
||||
expect(draftBaseIsStale('script', { hash: 'v2', draft: {} })).toBe(false)
|
||||
expect(draftBaseIsStale('script', { draft: { parent_hash: 'v1' } })).toBe(false)
|
||||
expect(draftBaseIsStale('script', { draft_base: 'v1' })).toBe(false)
|
||||
})
|
||||
|
||||
it('flow: compares the pinned version_id against the deployed head', () => {
|
||||
expect(draftBaseIsStale('flow', { version_id: 7, draft: { version_id: 5 } })).toBe(true)
|
||||
expect(draftBaseIsStale('flow', { version_id: 7, draft: { version_id: 7 } })).toBe(false)
|
||||
expect(draftBaseIsStale('flow', { version_id: 7, draft: {} })).toBe(false)
|
||||
it('flow: compares draft_base against the deployed version_id as text', () => {
|
||||
expect(draftBaseIsStale('flow', { version_id: 7, draft_base: '5' })).toBe(true)
|
||||
expect(draftBaseIsStale('flow', { version_id: 7, draft_base: '7' })).toBe(false)
|
||||
expect(draftBaseIsStale('flow', { version_id: 7 })).toBe(false)
|
||||
})
|
||||
|
||||
it('app/raw_app: compares parent_version against the last of versions', () => {
|
||||
expect(draftBaseIsStale('app', { versions: [1, 2, 3], draft: { parent_version: 2 } })).toBe(
|
||||
true
|
||||
)
|
||||
expect(draftBaseIsStale('raw_app', { versions: [1, 2, 3], draft: { parent_version: 3 } })).toBe(
|
||||
false
|
||||
)
|
||||
expect(draftBaseIsStale('app', { versions: [], draft: { parent_version: 2 } })).toBe(false)
|
||||
it('app/raw_app: compares draft_base against the last of versions', () => {
|
||||
expect(draftBaseIsStale('app', { versions: [1, 2, 3], draft_base: '2' })).toBe(true)
|
||||
expect(draftBaseIsStale('raw_app', { versions: [1, 2, 3], draft_base: '3' })).toBe(false)
|
||||
expect(draftBaseIsStale('app', { versions: [], draft_base: '2' })).toBe(false)
|
||||
})
|
||||
|
||||
it('no draft on the response → not stale', () => {
|
||||
|
||||
@@ -356,24 +356,24 @@ export async function getDraftDiffValues(
|
||||
|
||||
/**
|
||||
* Whether a draft's base is stale: the deployed version the draft forked from
|
||||
* no longer matches the current deployed head — a newer version was deployed
|
||||
* after the draft began, so deploying the draft would silently revert it.
|
||||
* Scripts compare the draft's `parent_hash` vs the deployed `hash`; flows the
|
||||
* pinned `version_id` vs the deployed head `version_id`; apps (incl. raw) the
|
||||
* pinned `parent_version` vs the head of `versions`. `r` is the item fetched
|
||||
* with `get_draft=true`; only script/flow/app kinds carry a base pointer.
|
||||
* (`draft_base`, text whatever the kind) no longer matches the current deployed
|
||||
* head — a newer version was deployed after the draft began, so deploying the
|
||||
* draft would silently revert it. The head is the deployed `hash` for scripts,
|
||||
* `version_id` for flows, the last of `versions` for apps (incl. raw). `r` is
|
||||
* the item fetched with `get_draft=true`.
|
||||
*/
|
||||
export function draftBaseIsStale(draftKind: UserDraftItemKind, r: any): boolean {
|
||||
const draft = r?.draft
|
||||
if (!draft) return false
|
||||
if (draftKind === 'script') {
|
||||
return !!r.hash && !!draft.parent_hash && draft.parent_hash !== r.hash
|
||||
}
|
||||
if (draftKind === 'flow') {
|
||||
return r.version_id != null && draft.version_id != null && draft.version_id !== r.version_id
|
||||
}
|
||||
const head = Array.isArray(r.versions) ? r.versions[r.versions.length - 1] : undefined
|
||||
return head != null && draft.parent_version != null && draft.parent_version !== head
|
||||
const base = r?.draft_base
|
||||
if (base == null) return false
|
||||
const head =
|
||||
draftKind === 'script'
|
||||
? r.hash
|
||||
: draftKind === 'flow'
|
||||
? r.version_id
|
||||
: Array.isArray(r.versions)
|
||||
? r.versions[r.versions.length - 1]
|
||||
: undefined
|
||||
return head != null && String(head) !== base
|
||||
}
|
||||
|
||||
/** Fetch-and-test wrapper over `draftBaseIsStale` for one draft item. Returns
|
||||
|
||||
@@ -52,8 +52,8 @@
|
||||
let deployedAt = $state<string | undefined>(undefined)
|
||||
// The app_version the draft was forked from (pinned), + the deployed head, for
|
||||
// the precise staleness check in DraftEditorModals (vs the drifting timestamp).
|
||||
let draftBaseVersion = $state<number | undefined>(undefined)
|
||||
let deployedHeadVersion = $state<number | undefined>(undefined)
|
||||
let draftBaseVersion = $state<string | undefined>(undefined)
|
||||
let deployedHeadVersion = $state<string | undefined>(undefined)
|
||||
|
||||
/** Increments per `loadApp` call. Stale loads (e.g. when picker
|
||||
* navigation races a draft-discard reload) bail at the next checkpoint
|
||||
@@ -289,13 +289,14 @@
|
||||
// `no_deployed` — no baseline to be older than.
|
||||
draftSavedAt = backendApp.draft_saved_at as string | undefined
|
||||
deployedAt = backendApp.no_deployed ? undefined : (backendApp.created_at as string | undefined)
|
||||
// `parent_version` rides on the persisted draft (pinned at fork); undefined
|
||||
// for a pre-feature draft. Head = the last entry of the deployed `versions`.
|
||||
draftBaseVersion = savedDraftApp?.parent_version
|
||||
deployedHeadVersion =
|
||||
// The app_version the draft forked from; undefined for a draft never forked
|
||||
// from a deploy. Head = the last entry of the deployed `versions`.
|
||||
draftBaseVersion = backendApp.draft_base
|
||||
const headVersion =
|
||||
backendApp.no_deployed || !backendApp.versions
|
||||
? undefined
|
||||
: backendApp.versions[backendApp.versions.length - 1]
|
||||
deployedHeadVersion = headVersion != null ? String(headVersion) : undefined
|
||||
const backendApp_ = structuredClone(stateSnapshot(backendApp))
|
||||
savedApp = {
|
||||
summary: backendApp_.summary,
|
||||
|
||||
@@ -58,7 +58,7 @@
|
||||
let deployedAt = $state<string | undefined>(undefined)
|
||||
// The flow_version the draft was forked from (pinned, doesn't drift), for the
|
||||
// precise staleness check in DraftEditorModals + FlowBuilder's deploy guard.
|
||||
let draftBaseVersion = $state<number | undefined>(undefined)
|
||||
let draftBaseVersion = $state<string | undefined>(undefined)
|
||||
// Editor-displayed path; defaults to the URL path. Cleared to '' in the
|
||||
// `new_draft` branch so the Path widget's `initPath` seeds the friendly name.
|
||||
let flowInitialPath = $state(page.params.path ?? '')
|
||||
@@ -372,9 +372,9 @@
|
||||
// Layer the draft (`.draft`, if any) over the deployed payload at the field
|
||||
// level. See /scripts/edit's loader for the rationale.
|
||||
const { draft: draftFromBackend, ...deployedFlow } = backendFlow as any
|
||||
// `version_id` rides on the persisted draft (pinned at fork); undefined for a
|
||||
// pre-feature draft or when editing the deployed flow directly (no draft).
|
||||
draftBaseVersion = draftFromBackend?.version_id as number | undefined
|
||||
// The flow_version the draft forked from; undefined when editing the deployed
|
||||
// flow directly (no draft) or for a draft never forked from a deploy.
|
||||
draftBaseVersion = backendFlow.draft_base
|
||||
const effectiveFlow: Flow = draftFromBackend
|
||||
? ({ ...deployedFlow, ...draftFromBackend } as Flow)
|
||||
: (deployedFlow as Flow)
|
||||
@@ -520,7 +520,7 @@
|
||||
{draftSavedAt}
|
||||
{deployedAt}
|
||||
{draftBaseVersion}
|
||||
deployedHeadVersion={version}
|
||||
deployedHeadVersion={version != null ? String(version) : undefined}
|
||||
onViewDiff={() => flowBuilder?.openDiffDrawer()}
|
||||
onLoadLatestDeploy={async () => {
|
||||
// stopSync-bracketed; see /scripts/edit's restoreDeployed for the race.
|
||||
|
||||
@@ -333,7 +333,7 @@
|
||||
// Exact staleness, preferred over the timestamps: a draft carried across
|
||||
// a move keeps its old save time while the move mints a fresh deploy, so
|
||||
// the timestamps alone would call every carried draft stale.
|
||||
draftBaseHash = draftFromBackend?.parent_hash as string | undefined
|
||||
draftBaseHash = backendScript.draft_base
|
||||
deployedHeadHash = backendScript.hash as string | undefined
|
||||
const effectiveScript: EditableScript = draftFromBackend
|
||||
? { ...deployedScript, ...draftFromBackend }
|
||||
@@ -343,7 +343,7 @@
|
||||
// it (by discarding or rebasing). Seeding it from the head here would let
|
||||
// the next autosave persist the head as the base, so a draft behind the
|
||||
// deploy reads as up to date after one open.
|
||||
const parentHash = topHash ?? draftFromBackend?.parent_hash ?? backendScript.hash
|
||||
const parentHash = topHash ?? backendScript.draft_base ?? backendScript.hash
|
||||
// Baseline for the autosave `discardIf`: the deployed script with the
|
||||
// same `parent_hash` graft the seed below applies, so the unedited draft
|
||||
// compares equal. `undefined` when there's no deployed row.
|
||||
|
||||
Reference in New Issue
Block a user