fix(drafts): deploy only wipes the deployer's draft, not everyone else's

Script / flow / app deploys ran an unconditional DELETE on every draft
at the path, so a teammate's deploy silently destroyed any other
user's pending draft. After the wipe, the other user's tab kept
auto-saving — re-creating the row at a NOW timestamp newer than the
deploy — and StaleDraftModal never fired because draft_saved_at had
been bumped past the deploy. Filter the DELETE to email = deployer
(plus the legacy NULL row), so other users' drafts persist and the
stale-draft prompt actually fires on their next reload.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Diego Imbert
2026-06-09 20:36:45 +02:00
parent eb2f90ff78
commit e3511b040a
6 changed files with 92 additions and 16 deletions
@@ -0,0 +1,16 @@
{
"db_name": "PostgreSQL",
"query": "DELETE FROM draft WHERE path = $1 AND workspace_id = $2 AND typ = 'flow' AND (email = $3 OR email IS NULL)",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Text",
"Text",
"Text"
]
},
"nullable": []
},
"hash": "3d7456e11d8686210169bfe26931f924bd4483c8f70095c05720bc4eb2d8df81"
}
@@ -0,0 +1,16 @@
{
"db_name": "PostgreSQL",
"query": "DELETE FROM draft WHERE path = $1 AND workspace_id = $2 AND typ = 'app' AND (email = $3 OR email IS NULL)",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Text",
"Text",
"Text"
]
},
"nullable": []
},
"hash": "8bba7bec4f5f09a90da3eecaab4cf4386eb97e4e00c7431ff2860b225d212780"
}
@@ -0,0 +1,16 @@
{
"db_name": "PostgreSQL",
"query": "DELETE FROM draft WHERE path = $1 AND workspace_id = $2 AND typ = 'script' AND (email = $3 OR email IS NULL)",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Text",
"Text",
"Text"
]
},
"nullable": []
},
"hash": "c241ee7efe2cbb9024792f6dc67cde48c5517ab36acd543a1f0a6119c34ce453"
}
+15 -6
View File
@@ -657,12 +657,17 @@ async fn create_flow(
).execute(&mut *tx).await?;
// CLI / git-sync deploys ask us to preserve any existing user draft at this
// path instead of wiping it as part of the deploy.
// path instead of wiping it as part of the deploy. Only wipe the deployer's
// own draft (plus the legacy NULL-email workspace draft) — other users'
// drafts are independent and should fire the StaleDraftModal on their next
// reload, not vanish silently.
if !nf.skip_draft_deletion.unwrap_or(false) {
sqlx::query!(
"DELETE FROM draft WHERE path = $1 AND workspace_id = $2 AND typ = 'flow'",
"DELETE FROM draft WHERE path = $1 AND workspace_id = $2 AND typ = 'flow' \
AND (email = $3 OR email IS NULL)",
nf.path,
&w_id
&w_id,
&authed.email,
)
.execute(&mut *tx)
.await?;
@@ -1246,12 +1251,16 @@ async fn update_flow(
}
// CLI / git-sync deploys ask us to preserve any existing user draft at this
// path instead of wiping it as part of the deploy.
// path instead of wiping it as part of the deploy. Only wipe the deployer's
// own draft (plus the legacy NULL-email row) — other users' drafts surface
// as stale on their next reload instead of disappearing silently.
if !nf.skip_draft_deletion.unwrap_or(false) {
sqlx::query!(
"DELETE FROM draft WHERE path = $1 AND workspace_id = $2 AND typ = 'flow'",
"DELETE FROM draft WHERE path = $1 AND workspace_id = $2 AND typ = 'flow' \
AND (email = $3 OR email IS NULL)",
flow_path,
&w_id
&w_id,
&authed.email,
)
.execute(&mut *tx)
.await?;
+16 -4
View File
@@ -1353,10 +1353,18 @@ async fn create_script_internal<'c>(
let p_path_opt = parent_hashes_and_perms.as_ref().map(|x| x.p_path.clone());
if let Some(ref p_path) = p_path_opt {
if !skip_draft_deletion {
// Only wipe the deployer's own draft (plus the legacy
// NULL-email workspace draft, if any). Other users' drafts
// are independent — they should NOT vanish silently when a
// teammate deploys. The home-page badge surfaces them, and
// the StaleDraftModal fires on their next reload because
// the draft now predates the new deploy.
sqlx::query!(
"DELETE FROM draft WHERE path = $1 AND workspace_id = $2 AND typ = 'script'",
"DELETE FROM draft WHERE path = $1 AND workspace_id = $2 AND typ = 'script' \
AND (email = $3 OR email IS NULL)",
p_path,
&w_id
&w_id,
&authed.email,
)
.execute(&mut *tx)
.await?;
@@ -1440,10 +1448,14 @@ async fn create_script_internal<'c>(
}
}
} else if !skip_draft_deletion {
// See the matching branch above — only wipe the deployer's own
// draft (plus the legacy NULL-email row).
sqlx::query!(
"DELETE FROM draft WHERE path = $1 AND workspace_id = $2 AND typ = 'script'",
"DELETE FROM draft WHERE path = $1 AND workspace_id = $2 AND typ = 'script' \
AND (email = $3 OR email IS NULL)",
ns.path,
&w_id
&w_id,
&authed.email,
)
.execute(&mut *tx)
.await?;
+13 -6
View File
@@ -1448,12 +1448,16 @@ async fn create_app_internal<'a>(
}
}
// CLI / git-sync deploys ask us to preserve any existing user draft at this
// path instead of wiping it as part of the deploy.
// path instead of wiping it as part of the deploy. Only wipe the deployer's
// own draft (plus the legacy NULL-email row) — other users' drafts surface
// as stale on their next reload instead of disappearing silently.
if !app.skip_draft_deletion.unwrap_or(false) {
sqlx::query!(
"DELETE FROM draft WHERE path = $1 AND workspace_id = $2 AND typ = 'app'",
"DELETE FROM draft WHERE path = $1 AND workspace_id = $2 AND typ = 'app' \
AND (email = $3 OR email IS NULL)",
&app.path,
&w_id
&w_id,
&authed.email,
)
.execute(&mut *tx)
.await?;
@@ -2057,12 +2061,15 @@ async fn update_app_internal<'a>(
}
};
// CLI / git-sync deploys ask us to preserve any existing user draft at this
// path instead of wiping it as part of the deploy.
// path instead of wiping it as part of the deploy. Only wipe the deployer's
// own draft (plus the legacy NULL-email row) — see create_app_internal.
if !ns.skip_draft_deletion.unwrap_or(false) {
sqlx::query!(
"DELETE FROM draft WHERE path = $1 AND workspace_id = $2 AND typ = 'app'",
"DELETE FROM draft WHERE path = $1 AND workspace_id = $2 AND typ = 'app' \
AND (email = $3 OR email IS NULL)",
path,
&w_id
&w_id,
&authed.email,
)
.execute(&mut *tx)
.await?;