diff --git a/backend/tests/drafts_move_taken.rs b/backend/tests/drafts_move_taken.rs index c1d713c8a0..40318ca2ef 100644 --- a/backend/tests/drafts_move_taken.rs +++ b/backend/tests/drafts_move_taken.rs @@ -215,7 +215,23 @@ async fn test_draft_move_refuses_another_users_other_app_kind( "a classic app was moved onto another user's raw app: {body}" ); assert!( - body.contains("Another user has a raw app draft"), + body.contains("holds another user's raw app draft"), + "the refusal did not name the occupant: {body}" + ); + + // And the other direction, where the occupant reads as the classic kind. + let (status, body) = move_to( + "raw_app", + "u/test-user/mvtaken_raw", + "u/test-user/mvtaken_app_theirs", + ) + .await?; + assert_eq!( + status, 400, + "a raw app was moved onto another user's classic app: {body}" + ); + assert!( + body.contains("holds another user's app draft"), "the refusal did not name the occupant: {body}" ); diff --git a/backend/tests/fixtures/drafts_move_taken.sql b/backend/tests/fixtures/drafts_move_taken.sql index 13d1fa3e90..5fee11b942 100644 --- a/backend/tests/fixtures/drafts_move_taken.sql +++ b/backend/tests/fixtures/drafts_move_taken.sql @@ -32,4 +32,6 @@ INSERT INTO draft (workspace_id, path, typ, value, email) VALUES ('test-workspace', 'u/test-user/mvtaken_raw', 'raw_app', '{"summary": "raw", "files": {}}', 'test@windmill.dev'), ('test-workspace', 'u/test-user/mvtaken_theirs', 'raw_app', - '{"summary": "theirs", "files": {}}', 'test2@windmill.dev'); + '{"summary": "theirs", "files": {}}', 'test2@windmill.dev'), + ('test-workspace', 'u/test-user/mvtaken_app_theirs', 'app', + '{"summary": "theirs", "value": {}}', 'test2@windmill.dev'); diff --git a/backend/windmill-api/src/drafts.rs b/backend/windmill-api/src/drafts.rs index 17ded64c45..40e2a3cb65 100644 --- a/backend/windmill-api/src/drafts.rs +++ b/backend/windmill-api/src/drafts.rs @@ -859,8 +859,8 @@ async fn move_draft( // The other app kind, owned by someone else: one deployed path cannot hold // both, so this is the other item's path, not a teammate's copy of this one. format!( - "Another user has a {occupant} draft at '{new_path}', and an app and a \ - raw app cannot share a path." + "'{new_path}' holds another user's {occupant} draft, and an app and a raw \ + app cannot share a path." ) } } else { diff --git a/frontend/src/lib/components/sessions/sessionRuntime.svelte.ts b/frontend/src/lib/components/sessions/sessionRuntime.svelte.ts index ab1b06f6e2..e67b0d15c3 100644 --- a/frontend/src/lib/components/sessions/sessionRuntime.svelte.ts +++ b/frontend/src/lib/components/sessions/sessionRuntime.svelte.ts @@ -593,8 +593,9 @@ function createRuntime(session: Session): SessionRuntime { // when the path has never been deployed. const aiDraft = UserDraft.get('flow', path, { workspace }) - // getDraft=true omits version_id (the plain getFlowByPath has it) — - // stamp it on so the flow doesn't always diff. Best-effort. + // Fallback for the head: the payload fetches below carry `version_id`, and + // this one covers a response that does not. Best-effort, and a request of + // its own, so the same response wins wherever both are available. let deployedVersionId: number | undefined try { deployedVersionId = (await FlowService.getFlowByPath({ workspace, path }))?.version_id @@ -608,9 +609,12 @@ function createRuntime(session: Session): SessionRuntime { // yet on the backend — draft-only flows are a valid state. try { const result = await FlowService.getFlowByPath({ workspace, path, getDraft: true }) - // `getDraft` omits `version_id`; the editor's deploy guard compares - // against it, so put the head fetched above back on the baseline. - saved.val = { ...(result as SavedFlow), version_id: deployedVersionId } + // The editor's deploy guard compares against the head, so keep this + // response's own and fall back to the one fetched above. + saved.val = { + ...(result as SavedFlow), + version_id: (result as SavedFlow).version_id ?? deployedVersionId + } } catch { saved.val = undefined } @@ -622,7 +626,10 @@ function createRuntime(session: Session): SessionRuntime { // No local draft yet — seed from `result.draft ?? result`. const result = await FlowService.getFlowByPath({ workspace, path, getDraft: true }) - saved.val = { ...(result as SavedFlow), version_id: deployedVersionId } + saved.val = { + ...(result as SavedFlow), + version_id: (result as SavedFlow).version_id ?? deployedVersionId + } const serverDraft = (result as SavedFlow).draft as Flow | undefined const flow: Flow = (serverDraft ?? (result as Flow)) as Flow // Seed the per-tab last_sync from the server draft's timestamp so the