From 0fa5d61d69f0efa1f23a8c366eafd0e097e40fe3 Mon Sep 17 00:00:00 2001 From: Diego Imbert Date: Wed, 27 May 2026 16:36:44 +0200 Subject: [PATCH] Db draft removal --- ...1f7f387f5055c47f493271d26731336257384.json | 10 +- backend/windmill-api-flows/src/flows.rs | 95 ------- .../tests/apps.rs | 6 - .../tests/drafts.rs | 105 -------- .../tests/flows.rs | 26 +- .../tests/scripts.rs | 6 - backend/windmill-api-scripts/src/scripts.rs | 166 +----------- backend/windmill-api/openapi.yaml | 152 ----------- backend/windmill-api/src/apps.rs | 83 ------ backend/windmill-api/src/drafts.rs | 138 ---------- backend/windmill-api/src/lib.rs | 2 - backend/windmill-api/src/workspaces_export.rs | 1 - backend/windmill-types/src/flows.rs | 1 - backend/windmill-types/src/scripts.rs | 2 - cli/package-lock.json | 7 + frontend/src/lib/components/DiffDrawer.svelte | 2 +- frontend/src/lib/components/DraftBadge.svelte | 29 -- .../src/lib/components/FlowBuilder.svelte | 193 +------------- .../src/lib/components/ScriptBuilder.svelte | 215 +-------------- .../apps/editor/AppEditorHeader.svelte | 236 +---------------- .../AppEditorHeaderDeployInitialDraft.svelte | 55 ---- .../apps/editor/AppJsonEditor.svelte | 38 +-- .../lib/components/common/table/AppRow.svelte | 29 +- .../components/common/table/FlowRow.svelte | 29 +- .../components/common/table/ScriptRow.svelte | 25 +- .../copilot/chat/global/core.test.ts | 164 ++++-------- .../components/copilot/chat/global/core.ts | 41 +-- frontend/src/lib/components/diff_drawer.ts | 42 +-- frontend/src/lib/components/flow_builder.ts | 19 +- .../flows/content/FlowEditorDrawer.svelte | 21 +- .../src/lib/components/home/ItemsList.svelte | 1 - .../src/lib/components/home/treeViewUtils.ts | 1 - .../raw_apps/RawAppEditorHeader.svelte | 248 +----------------- frontend/src/lib/components/script_builder.ts | 10 +- .../search/GlobalSearchModal.svelte | 1 - frontend/src/lib/components/triggers/utils.ts | 67 +---- .../(logged)/apps/edit/[...path]/+page.svelte | 130 ++------- .../apps_raw/edit/[...path]/+page.svelte | 110 ++------ .../(root)/(logged)/flows/add/+page.svelte | 5 - .../flows/edit/[...path]/+page.svelte | 121 +-------- .../(root)/(logged)/scripts/add/+page.svelte | 4 - .../scripts/edit/[...path]/+page.svelte | 112 +------- 42 files changed, 242 insertions(+), 2506 deletions(-) delete mode 100644 backend/windmill-api-integration-tests/tests/drafts.rs delete mode 100644 backend/windmill-api/src/drafts.rs delete mode 100644 frontend/src/lib/components/DraftBadge.svelte delete mode 100644 frontend/src/lib/components/apps/editor/AppEditorHeaderDeployInitialDraft.svelte diff --git a/backend/.sqlx/query-07168aaf14cb6beff0ad4274b441f7f387f5055c47f493271d26731336257384.json b/backend/.sqlx/query-07168aaf14cb6beff0ad4274b441f7f387f5055c47f493271d26731336257384.json index d29a18c691..e7ed0aee65 100644 --- a/backend/.sqlx/query-07168aaf14cb6beff0ad4274b441f7f387f5055c47f493271d26731336257384.json +++ b/backend/.sqlx/query-07168aaf14cb6beff0ad4274b441f7f387f5055c47f493271d26731336257384.json @@ -46,11 +46,11 @@ ] }, "nullable": [ - true, - true, - true, - true, - true, + false, + false, + false, + false, + false, true, true ] diff --git a/backend/windmill-api-flows/src/flows.rs b/backend/windmill-api-flows/src/flows.rs index 8a42bce88e..4ca036c40c 100644 --- a/backend/windmill-api-flows/src/flows.rs +++ b/backend/windmill-api-flows/src/flows.rs @@ -48,7 +48,6 @@ use windmill_common::{ flows::{Flow, FlowWithStarred, ListFlowQuery, ListableFlow, NewFlow}, jobs::JobPayload, schedule::Schedule, - scripts::Schema, utils::{http_get_from_hub, not_found_if_none, paginate, Pagination, RunnableKind, StripPath}, }; use windmill_dep_map::scoped_dependency_map::ScopedDependencyMap; @@ -67,7 +66,6 @@ pub fn workspaced_service() -> Router { .route("/list_tokens/{*path}", get(list_tokens)) .route("/get/{*path}", get(get_flow_by_path)) .route("/deployment_status/p/{*path}", get(get_deployment_status)) - .route("/get/draft/{*path}", get(get_flow_by_path_w_draft)) .route("/exists/{*path}", get(exists_flow_by_path)) .route("/list_paths", get(list_paths)) .route("/history/p/{*path}", get(get_flow_history)) @@ -148,7 +146,6 @@ async fn list_flows( "archived", "extra_perms", "favorite.path IS NOT NULL as starred", - "draft.path IS NOT NULL as has_draft", "draft_only", "ws_error_handler_muted", "o.labels" @@ -160,11 +157,6 @@ async fn list_flows( .bind(&authed.username), ) .left() - .join("draft") - .on( - "draft.path = o.path AND draft.workspace_id = o.workspace_id AND draft.typ = 'flow'" - ) - .left() .join("flow_version fv") .on( "fv.id = o.versions[array_upper(o.versions, 1)]" @@ -703,18 +695,6 @@ async fn check_schedule_conflict<'c>( Ok(()) } -pub async fn require_is_writer(authed: &ApiAuthed, path: &str, w_id: &str, db: DB) -> Result<()> { - return windmill_api_auth::require_is_writer( - authed, - path, - w_id, - db, - "SELECT extra_perms FROM flow WHERE path = $1 AND workspace_id = $2", - "flow", - ) - .await; -} - #[derive(Serialize)] pub struct FlowVersion { pub id: i64, @@ -1470,81 +1450,6 @@ async fn get_flow_by_path( Ok(Json(flow)) } -#[derive(Serialize, sqlx::FromRow)] -pub struct FlowWDraft { - pub path: String, - pub summary: String, - pub description: String, - pub schema: Option, - pub value: sqlx::types::Json>, - pub extra_perms: serde_json::Value, - #[serde(skip_serializing_if = "Option::is_none")] - pub draft: Option>>, - /// Timestamp at which the most recent DB draft was created. - #[serde(skip_serializing_if = "Option::is_none")] - pub draft_created_at: Option>, - #[serde(skip_serializing_if = "Option::is_none")] - pub draft_only: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub tag: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub ws_error_handler_muted: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub dedicated_worker: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub visible_to_runner_only: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub on_behalf_of_email: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub labels: Option>, -} - -async fn get_flow_by_path_w_draft( - authed: ApiAuthed, - Extension(user_db): Extension, - Path((w_id, path)): Path<(String, StripPath)>, -) -> JsonResult { - let path = path.to_path(); - check_scopes(&authed, || format!("flows:read:{}", path))?; - let mut tx = user_db.begin(&authed).await?; - let flow_o = sqlx::query_as::<_, FlowWDraft>( - "SELECT - flow.path, - flow.summary, - flow.description, - flow_version.schema, - flow_version.value, - flow.extra_perms, - flow.draft_only, - flow.ws_error_handler_muted, - flow.dedicated_worker, - draft.value AS draft, - draft.created_at AS draft_created_at, - flow.tag, - flow.visible_to_runner_only, - flow.on_behalf_of_email, - flow.labels - FROM flow - LEFT JOIN draft - ON flow.path = draft.path - AND draft.workspace_id = $2 - AND draft.typ = 'flow' - LEFT JOIN flow_version - ON flow_version.id = flow.versions[array_upper(flow.versions, 1)] - WHERE flow.path = $1 - AND flow.workspace_id = $2", - ) - .bind(path) - .bind(w_id) - .fetch_optional(&mut *tx) - .await?; - - tx.commit().await?; - - let flow = not_found_if_none(flow_o, "Flow", path)?; - Ok(Json(flow)) -} - async fn exists_flow_by_path( Extension(db): Extension, Path((w_id, path)): Path<(String, StripPath)>, diff --git a/backend/windmill-api-integration-tests/tests/apps.rs b/backend/windmill-api-integration-tests/tests/apps.rs index 63bd96345b..3d70c09ce1 100644 --- a/backend/windmill-api-integration-tests/tests/apps.rs +++ b/backend/windmill-api-integration-tests/tests/apps.rs @@ -82,12 +82,6 @@ async fn test_app_endpoints(db: Pool) -> anyhow::Result<()> { let resp = authed_get(port, "get/p", "u/test-user/nonexistent").await; assert_eq!(resp.status(), 404); - // --- get draft --- - let resp = authed_get(port, "get/draft", "u/test-user/test_app").await; - assert_eq!(resp.status(), 200); - let body = resp.json::().await?; - assert_eq!(body["path"], "u/test-user/test_app"); - // --- get lite --- let resp = authed_get(port, "get/lite", "u/test-user/test_app").await; assert_eq!(resp.status(), 200); diff --git a/backend/windmill-api-integration-tests/tests/drafts.rs b/backend/windmill-api-integration-tests/tests/drafts.rs deleted file mode 100644 index d86c91b6a1..0000000000 --- a/backend/windmill-api-integration-tests/tests/drafts.rs +++ /dev/null @@ -1,105 +0,0 @@ -use serde_json::json; -use sqlx::{Pool, Postgres}; - -use windmill_test_utils::*; - -fn client() -> reqwest::Client { - reqwest::Client::new() -} - -fn authed(builder: reqwest::RequestBuilder) -> reqwest::RequestBuilder { - builder.header("Authorization", "Bearer SECRET_TOKEN") -} - -#[sqlx::test(migrations = "../migrations", fixtures("base"))] -async fn test_draft_endpoints(db: Pool) -> anyhow::Result<()> { - initialize_tracing().await; - let server = ApiServer::start(db.clone()).await?; - let port = server.addr.port(); - let base = format!("http://localhost:{port}/api/w/test-workspace/drafts"); - - // create a script first so the draft has a valid path - let resp = authed(client().post(format!( - "http://localhost:{port}/api/w/test-workspace/scripts/create" - ))) - .json(&json!({ - "path": "u/test-user/draft_script", - "summary": "Script for draft test", - "description": "", - "content": "export async function main() { return 1; }", - "language": "deno", - "schema": { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "type": "object", - "properties": {}, - "required": [] - } - })) - .send() - .await - .unwrap(); - assert_eq!(resp.status(), 201, "create script: {}", resp.text().await?); - - // --- create draft --- - let resp = authed(client().post(format!("{base}/create"))) - .json(&json!({ - "path": "u/test-user/draft_script", - "typ": "script", - "value": { - "content": "export async function main() { return 2; }", - "language": "deno" - } - })) - .send() - .await - .unwrap(); - assert_eq!(resp.status(), 201, "create draft: {}", resp.text().await?); - - // verify draft exists via script get/draft endpoint - let resp = authed(client().get(format!( - "http://localhost:{port}/api/w/test-workspace/scripts/get/draft/u/test-user/draft_script" - ))) - .send() - .await - .unwrap(); - assert_eq!(resp.status(), 200); - let body = resp.json::().await?; - assert!(body["draft"].is_object(), "expected draft to be present"); - - // --- update draft (create with same path overwrites) --- - let resp = authed(client().post(format!("{base}/create"))) - .json(&json!({ - "path": "u/test-user/draft_script", - "typ": "script", - "value": { - "content": "export async function main() { return 3; }", - "language": "deno" - } - })) - .send() - .await - .unwrap(); - assert_eq!(resp.status(), 201); - - // --- delete draft --- - let resp = authed(client().delete(format!( - "{base}/delete/script/u/test-user/draft_script" - ))) - .send() - .await - .unwrap(); - assert_eq!(resp.status(), 200); - - // verify draft is gone - let resp = authed(client().get(format!( - "http://localhost:{port}/api/w/test-workspace/scripts/get/draft/u/test-user/draft_script" - ))) - .send() - .await - .unwrap(); - assert_eq!(resp.status(), 200); - let body = resp.json::().await?; - assert!(body["draft"].is_null(), "expected draft to be deleted"); - - Ok(()) -} diff --git a/backend/windmill-api-integration-tests/tests/flows.rs b/backend/windmill-api-integration-tests/tests/flows.rs index ff3f86bf2d..1d58edb5e7 100644 --- a/backend/windmill-api-integration-tests/tests/flows.rs +++ b/backend/windmill-api-integration-tests/tests/flows.rs @@ -82,12 +82,6 @@ async fn test_flow_endpoints(db: Pool) -> anyhow::Result<()> { let resp = authed_get(port, "get", "u/test-user/nonexistent").await; assert_eq!(resp.status(), 404); - // --- get draft --- - let resp = authed_get(port, "get/draft", "u/test-user/test_flow").await; - assert_eq!(resp.status(), 200); - let body = resp.json::().await?; - assert_eq!(body["path"], "u/test-user/test_flow"); - // --- list --- let resp = authed(client().get(format!("{base}/list"))) .send() @@ -259,12 +253,10 @@ async fn test_flow_endpoints(db: Pool) -> anyhow::Result<()> { // ===== Hub endpoints (require external network, expect 500 or 200) ===== // --- hub/list --- - let resp = authed(client().get(format!( - "http://localhost:{port}/api/flows/hub/list" - ))) - .send() - .await - .unwrap(); + let resp = authed(client().get(format!("http://localhost:{port}/api/flows/hub/list"))) + .send() + .await + .unwrap(); assert!( resp.status() == 200 || resp.status() == 500, "hub/list: unexpected status {}", @@ -272,12 +264,10 @@ async fn test_flow_endpoints(db: Pool) -> anyhow::Result<()> { ); // --- hub/get --- - let resp = authed(client().get(format!( - "http://localhost:{port}/api/flows/hub/get/1" - ))) - .send() - .await - .unwrap(); + let resp = authed(client().get(format!("http://localhost:{port}/api/flows/hub/get/1"))) + .send() + .await + .unwrap(); assert!( resp.status() == 200 || resp.status() == 500, "hub/get: unexpected status {}", diff --git a/backend/windmill-api-integration-tests/tests/scripts.rs b/backend/windmill-api-integration-tests/tests/scripts.rs index f5e78f880f..855c0bc9b4 100644 --- a/backend/windmill-api-integration-tests/tests/scripts.rs +++ b/backend/windmill-api-integration-tests/tests/scripts.rs @@ -98,12 +98,6 @@ async fn test_script_endpoints(db: Pool) -> anyhow::Result<()> { let body = resp.json::().await?; assert_eq!(body["path"], "u/test-user/test_script"); - // --- get draft --- - let resp = authed_get(port, "get/draft", "u/test-user/test_script").await; - assert_eq!(resp.status(), 200); - let body = resp.json::().await?; - assert_eq!(body["path"], "u/test-user/test_script"); - // --- raw by path (requires language extension) --- let resp = authed_get(port, "raw/p", "u/test-user/test_script.ts").await; assert_eq!(resp.status(), 200); diff --git a/backend/windmill-api-scripts/src/scripts.rs b/backend/windmill-api-scripts/src/scripts.rs index 19fbaa2148..d2a3bfdfa5 100644 --- a/backend/windmill-api-scripts/src/scripts.rs +++ b/backend/windmill-api-scripts/src/scripts.rs @@ -32,7 +32,6 @@ use itertools::Itertools; use quick_cache::sync::Cache; use serde::{Deserialize, Serialize}; use serde_json::json; -use serde_json::value::RawValue; use sql_builder::prelude::*; use sqlx::{FromRow, Postgres, Transaction}; use std::{collections::HashMap, sync::Arc}; @@ -44,7 +43,7 @@ use windmill_dep_map::scoped_dependency_map::ScopedDependencyMap; use windmill_common::{ assets::{ clear_static_asset_usage, clear_static_asset_usage_by_script_hash, - insert_static_asset_usage, AssetUsageKind, AssetWithAltAccessType, + insert_static_asset_usage, AssetUsageKind, }, error::{self, to_anyhow}, min_version::{MIN_VERSION_SUPPORTS_DEBOUNCING, MIN_VERSION_SUPPORTS_DEBOUNCING_V2}, @@ -81,122 +80,6 @@ use windmill_queue::{ const MAX_HASH_HISTORY_LENGTH_STORED: usize = 20; -#[derive(Serialize, sqlx::FromRow)] -pub struct ScriptWDraft { - pub hash: ScriptHash, - pub path: String, - pub summary: String, - pub description: String, - pub content: String, - pub language: ScriptLang, - pub kind: ScriptKind, - pub tag: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub draft: Option>>, - /// Timestamp at which the most recent DB draft was created. - #[serde(skip_serializing_if = "Option::is_none")] - pub draft_created_at: Option>, - pub schema: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub draft_only: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub envs: Option>, - #[serde(skip_serializing_if = "Option::is_none")] - pub cache_ttl: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub cache_ignore_s3_path: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub dedicated_worker: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub ws_error_handler_muted: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub priority: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub restart_unless_cancelled: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub delete_after_use: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub delete_after_secs: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub timeout: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub visible_to_runner_only: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub auto_kind: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub has_preprocessor: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub on_behalf_of_email: Option, - #[serde(skip_serializing_if = "Option::is_none")] - #[sqlx(json(nullable))] - pub assets: Option>, - #[serde(skip_serializing_if = "Option::is_none")] - #[sqlx(json(nullable))] - pub modules: Option>, - #[serde(skip_serializing_if = "Option::is_none")] - pub labels: Option>, - #[serde(flatten)] - #[sqlx(flatten)] - pub runnable_settings: SR, -} - -impl ScriptWDraft { - pub async fn prefetch_cached<'a>( - self, - db: &DB, - ) -> error::Result> { - let (debouncing_settings, concurrency_settings) = - windmill_common::runnable_settings::prefetch_cached_from_handle( - self.runnable_settings.runnable_settings_handle, - db, - ) - .await?; - - Ok(ScriptWDraft { - runnable_settings: ScriptRunnableSettingsInline { - concurrency_settings: concurrency_settings.maybe_fallback( - self.runnable_settings.concurrency_key, - self.runnable_settings.concurrent_limit, - self.runnable_settings.concurrency_time_window_s, - ), - debouncing_settings: debouncing_settings.maybe_fallback( - self.runnable_settings.debounce_key, - self.runnable_settings.debounce_delay_s, - ), - }, - hash: self.hash, - path: self.path, - summary: self.summary, - description: self.description, - content: self.content, - language: self.language, - kind: self.kind, - tag: self.tag, - draft: self.draft, - draft_created_at: self.draft_created_at, - schema: self.schema, - draft_only: self.draft_only, - envs: self.envs, - cache_ttl: self.cache_ttl, - cache_ignore_s3_path: self.cache_ignore_s3_path, - dedicated_worker: self.dedicated_worker, - ws_error_handler_muted: self.ws_error_handler_muted, - priority: self.priority, - restart_unless_cancelled: self.restart_unless_cancelled, - delete_after_use: self.delete_after_use, - delete_after_secs: self.delete_after_secs, - timeout: self.timeout, - visible_to_runner_only: self.visible_to_runner_only, - auto_kind: self.auto_kind, - has_preprocessor: self.has_preprocessor, - on_behalf_of_email: self.on_behalf_of_email, - assets: self.assets, - modules: self.modules, - labels: self.labels, - }) - } -} - pub fn global_service() -> Router { Router::new() .route("/hub/top", get(get_top_hub_scripts)) @@ -221,7 +104,6 @@ pub fn workspaced_service() -> Router { .route("/create", post(create_script)) .route("/create_snapshot", post(create_snapshot_script)) .route("/archive/p/{*path}", post(archive_script_by_path)) - .route("/get/draft/{*path}", get(get_script_by_path_w_draft)) .route("/get/p/{*path}", get(get_script_by_path)) .route("/list_tokens/{*path}", get(list_tokens)) .route("/raw/p/{*path}", get(raw_script_by_path)) @@ -302,7 +184,7 @@ async fn list_scripts( "hash", "o.path", "summary", - "COALESCE(draft.created_at, o.created_at) as created_at", + "o.created_at as created_at", "archived", "extra_perms", if !lq.without_description.unwrap_or(false) { @@ -314,7 +196,6 @@ async fn list_scripts( "language", "favorite.path IS NOT NULL as starred", "tag", - "draft.path IS NOT NULL as has_draft", "draft_only", "ws_error_handler_muted", "auto_kind", @@ -328,11 +209,6 @@ async fn list_scripts( "favorite.favorite_kind = 'script' AND favorite.workspace_id = o.workspace_id AND favorite.path = o.path AND favorite.usr = ?" .bind(&authed.username), ) - .left() - .join("draft") - .on( - "draft.path = o.path AND draft.workspace_id = o.workspace_id AND draft.typ = 'script'" - ) .order_desc("favorite.path IS NOT NULL") .order_by("created_at", lq.order_desc.unwrap_or(true)) .and_where("o.workspace_id = ?".bind(&w_id)) @@ -1814,32 +1690,6 @@ async fn list_tokens( list_tokens_internal(&db, &w_id, &path, false).await } -async fn get_script_by_path_w_draft( - authed: ApiAuthed, - Extension(db): Extension, - Extension(user_db): Extension, - Path((w_id, path)): Path<(String, StripPath)>, -) -> JsonResult> { - let path = path.to_path(); - check_scopes(&authed, || format!("scripts:read:{}", path))?; - let mut tx = user_db.begin(&authed).await?; - - let script_o = sqlx::query_as::<_, ScriptWDraft>( - "SELECT hash, script.path, summary, description, content, language, kind, tag, schema, draft_only, envs, runnable_settings_handle, concurrent_limit, concurrency_time_window_s, cache_ttl, cache_ignore_s3_path, ws_error_handler_muted, draft.value as draft, draft.created_at as draft_created_at, dedicated_worker, priority, restart_unless_cancelled, delete_after_use, delete_after_secs, timeout, concurrency_key, visible_to_runner_only, auto_kind, has_preprocessor, on_behalf_of_email, assets, modules, debounce_key, debounce_delay_s, labels FROM script LEFT JOIN draft ON - script.path = draft.path AND script.workspace_id = draft.workspace_id AND draft.typ = 'script' - WHERE script.path = $1 AND script.workspace_id = $2 - ORDER BY script.created_at DESC LIMIT 1", - ) - .bind(path) - .bind(w_id) - .fetch_optional(&mut *tx) - .await?; - tx.commit().await?; - - let script = not_found_if_none(script_o, "Script", path)?; - Ok(Json(script.prefetch_cached(&db).await?)) -} - async fn get_script_history( authed: ApiAuthed, Extension(user_db): Extension, @@ -2404,18 +2254,6 @@ async fn get_deployment_status( Ok(Json(deployment_status)) } -pub async fn require_is_writer(authed: &ApiAuthed, path: &str, w_id: &str, db: DB) -> Result<()> { - return windmill_api_auth::require_is_writer( - authed, - path, - w_id, - db, - "SELECT extra_perms FROM script WHERE path = $1 AND workspace_id = $2 ORDER BY created_at DESC LIMIT 1", - "script", - ) - .await; -} - async fn archive_script_by_path( authed: ApiAuthed, Extension(webhook): Extension, diff --git a/backend/windmill-api/openapi.yaml b/backend/windmill-api/openapi.yaml index 0d21853205..e4fd8e524d 100644 --- a/backend/windmill-api/openapi.yaml +++ b/backend/windmill-api/openapi.yaml @@ -7802,65 +7802,6 @@ paths: items: type: string - /w/{workspace}/drafts/create: - post: - summary: create draft - operationId: createDraft - tags: - - draft - parameters: - - $ref: "#/components/parameters/WorkspaceId" - requestBody: - required: true - content: - application/json: - schema: - type: object - properties: - path: - type: string - typ: - type: string - enum: ["flow", "script", "app"] - value: {} - required: - - path - - typ - - enum - responses: - "201": - description: draft created - content: - text/plain: - schema: - type: string - - /w/{workspace}/drafts/delete/{kind}/{path}: - delete: - summary: delete draft - operationId: deleteDraft - tags: - - draft - parameters: - - $ref: "#/components/parameters/WorkspaceId" - - name: kind - in: path - required: true - schema: - type: string - enum: - - script - - flow - - app - - $ref: "#/components/parameters/ScriptPath" - responses: - "200": - description: draft deleted - content: - text/plain: - schema: - type: string - /w/{workspace}/scripts/create: post: summary: create script @@ -8282,23 +8223,6 @@ paths: items: $ref: "#/components/schemas/TruncatedToken" - /w/{workspace}/scripts/get/draft/{path}: - get: - summary: get script by path with draft - operationId: getScriptByPathWithDraft - tags: - - script - parameters: - - $ref: "#/components/parameters/WorkspaceId" - - $ref: "#/components/parameters/ScriptPath" - responses: - "200": - description: script details - content: - application/json: - schema: - $ref: "#/components/schemas/NewScriptWithDraft" - /w/{workspace}/scripts/history/p/{path}: get: summary: get history of a script by path @@ -9447,8 +9371,6 @@ paths: - $ref: "#/components/schemas/Flow" - type: object properties: - has_draft: - type: boolean draft_only: type: boolean @@ -9674,32 +9596,6 @@ paths: schema: type: string - /w/{workspace}/flows/get/draft/{path}: - get: - summary: get flow by path with draft - operationId: getFlowByPathWithDraft - tags: - - flow - parameters: - - $ref: "#/components/parameters/WorkspaceId" - - $ref: "#/components/parameters/ScriptPath" - responses: - "200": - description: flow details with draft - content: - application/json: - schema: - allOf: - - $ref: "#/components/schemas/Flow" - - type: object - properties: - draft: - $ref: "#/components/schemas/Flow" - draft_created_at: - type: string - format: date-time - description: Timestamp at which the most recent DB draft was created. Used by the frontend's UserDraft staleness check. - /w/{workspace}/flows/exists/{path}: get: summary: exists flow by path @@ -10414,23 +10310,6 @@ paths: schema: $ref: "#/components/schemas/AppWithLastVersion" - /w/{workspace}/apps/get/draft/{path}: - get: - summary: get app by path with draft - operationId: getAppByPathWithDraft - tags: - - app - parameters: - - $ref: "#/components/parameters/WorkspaceId" - - $ref: "#/components/parameters/ScriptPath" - responses: - "200": - description: app details with draft - content: - application/json: - schema: - $ref: "#/components/schemas/AppWithLastVersionWDraft" - /w/{workspace}/apps/history/p/{path}: get: summary: get app history by path @@ -21692,8 +21571,6 @@ components: type: boolean tag: type: string - has_draft: - type: boolean draft_only: type: boolean envs: @@ -21890,22 +21767,6 @@ components: - content - language - NewScriptWithDraft: - allOf: - - $ref: "#/components/schemas/NewScript" - - type: object - properties: - draft: - $ref: "#/components/schemas/NewScript" - draft_created_at: - type: string - format: date-time - description: Timestamp at which the most recent DB draft was created. Used by the frontend's UserDraft staleness check. - hash: - type: string - required: - - hash - ScriptHistory: type: object properties: @@ -26976,19 +26837,6 @@ components: - raw_app - AppWithLastVersionWDraft: - allOf: - - $ref: "#/components/schemas/AppWithLastVersion" - - type: object - properties: - draft_only: - type: boolean - draft: {} - draft_created_at: - type: string - format: date-time - description: Timestamp at which the most recent DB draft was created. Used by the frontend's UserDraft staleness check. - AppHistory: type: object properties: diff --git a/backend/windmill-api/src/apps.rs b/backend/windmill-api/src/apps.rs index c377203f38..07b6916aec 100644 --- a/backend/windmill-api/src/apps.rs +++ b/backend/windmill-api/src/apps.rs @@ -88,7 +88,6 @@ pub fn workspaced_service(raw_app_body_limit: usize) -> Router { .route("/list_search", get(list_search_apps)) .route("/get/p/{*path}", get(get_app)) .route("/get/lite/{*path}", get(get_app_lite)) - .route("/get/draft/{*path}", get(get_app_w_draft)) .route("/secret_of/{*path}", get(get_secret_id)) .route( "/secret_of_latest_version/{*path}", @@ -153,7 +152,6 @@ pub struct ListableApp { pub execution_mode: String, pub starred: bool, pub edited_at: Option>, - pub has_draft: bool, #[serde(skip_serializing_if = "Option::is_none")] pub draft_only: Option, #[sqlx(default)] @@ -208,20 +206,6 @@ pub struct AppWithLastVersionAndStarred { pub starred: Option, } -#[derive(Serialize, Deserialize, FromRow)] -pub struct AppWithLastVersionAndDraft { - #[sqlx(flatten)] - #[serde(flatten)] - pub app: AppWithLastVersion, - #[serde(skip_serializing_if = "Option::is_none")] - pub draft: Option>>, - #[serde(skip_serializing_if = "Option::is_none")] - pub draft_only: Option, - /// Timestamp at which the most recent DB draft was created. - #[serde(skip_serializing_if = "Option::is_none")] - pub draft_created_at: Option>, -} - #[derive(Serialize)] pub struct AppHistory { pub app_id: i64, @@ -371,7 +355,6 @@ async fn list_apps( "app_version.created_at as edited_at", "app.extra_perms", "favorite.path IS NOT NULL as starred", - "draft.path IS NOT NULL as has_draft", "draft_only", "app_version.raw_app", "app.labels", @@ -387,11 +370,6 @@ async fn list_apps( .on( "app_version.id = versions[array_upper(versions, 1)]" ) - .left() - .join("draft") - .on( - "draft.path = app.path AND draft.workspace_id = app.workspace_id AND draft.typ = 'app'" - ) .order_desc("favorite.path IS NOT NULL") .order_by("app_version.created_at", true) .and_where("app.workspace_id = ?".bind(&w_id)) @@ -634,55 +612,6 @@ async fn get_app_lite( Ok(Json(app)) } -async fn get_app_w_draft( - authed: ApiAuthed, - Extension(user_db): Extension, - Path((w_id, path)): Path<(String, StripPath)>, -) -> JsonResult { - let path = path.to_path(); - check_scopes(&authed, || format!("apps:read:{}", path))?; - let mut tx = user_db.begin(&authed).await?; - - let app_o = sqlx::query_as::<_, AppWithLastVersionAndDraft>( - r#" - SELECT - app.id, - app.path, - app.summary, - app.versions, - app.policy, - app.custom_path, - app.extra_perms, - app_version.value, - app_version.created_at, - app_version.created_by, - app.draft_only, - draft.value AS "draft", - draft.created_at AS "draft_created_at", - app_version.raw_app, - app.labels - FROM app - INNER JOIN app_version - ON app_version.id = app.versions[array_upper(app.versions, 1)] - LEFT JOIN draft - ON app.path = draft.path - AND draft.workspace_id = $2 - AND draft.typ = 'app' - WHERE app.path = $1 - AND app.workspace_id = $2 - "#, - ) - .bind(path.to_owned()) - .bind(&w_id) - .fetch_optional(&mut *tx) - .await?; - - tx.commit().await?; - - let app = not_found_if_none(app_o, "App", path)?; - Ok(Json(app)) -} - async fn get_app_history( authed: ApiAuthed, Extension(user_db): Extension, @@ -3190,18 +3119,6 @@ fn get_on_behalf_of(policy: &Policy) -> Result<(String, String)> { Ok((permissioned_as, email)) } -pub async fn require_is_writer(authed: &ApiAuthed, path: &str, w_id: &str, db: DB) -> Result<()> { - return crate::users::require_is_writer( - authed, - path, - w_id, - db, - "SELECT extra_perms FROM app WHERE path = $1 AND workspace_id = $2", - "app", - ) - .await; -} - async fn exists_app( Extension(db): Extension, Path((w_id, path)): Path<(String, StripPath)>, diff --git a/backend/windmill-api/src/drafts.rs b/backend/windmill-api/src/drafts.rs deleted file mode 100644 index b0cd5f61b0..0000000000 --- a/backend/windmill-api/src/drafts.rs +++ /dev/null @@ -1,138 +0,0 @@ -/* - * Author: Ruben Fiszel - * Copyright: Windmill Labs, Inc 2024 - * This file and its contents are licensed under the AGPLv3 License. - * Please see the included NOTICE for copyright information and - * LICENSE-AGPL for a copy of the license. - */ - -use crate::{ - db::{ApiAuthed, DB}, - users::{maybe_refresh_folders, require_owner_of_path}, -}; - -use axum::{ - extract::{Extension, Path}, - routing::{delete, post}, - Json, Router, -}; -use hyper::StatusCode; -use serde::{Deserialize, Serialize}; -use windmill_common::{db::UserDB, error::Result, utils::StripPath}; - -pub fn workspaced_service() -> Router { - Router::new() - .route("/create", post(create_draft)) - .route("/delete/{kind}/{*path}", delete(delete_draft)) -} - -#[derive(sqlx::Type, Serialize, Deserialize, Debug, PartialEq, Clone)] -#[sqlx(type_name = "DRAFT_TYPE", rename_all = "lowercase")] -#[serde(rename_all(serialize = "lowercase", deserialize = "lowercase"))] -pub enum DraftType { - Script, - Flow, - App, -} - -#[derive(Deserialize, Serialize, Debug)] -pub struct Draft { - pub path: String, - pub value: sqlx::types::Json>, - pub typ: DraftType, -} - -pub async fn require_writer_of_path( - authed: &ApiAuthed, - path: &str, - w_id: &str, - db: DB, - kind: &DraftType, -) -> Result<()> { - if authed.is_admin { - return Ok(()); - } else if require_owner_of_path(authed, path).is_ok() { - return Ok(()); - } else { - match kind { - DraftType::Script => crate::scripts::require_is_writer(authed, path, w_id, db).await, - DraftType::Flow => crate::flows::require_is_writer(authed, path, w_id, db).await, - DraftType::App => crate::apps::require_is_writer(authed, path, w_id, db).await, - } - } -} - -async fn create_draft( - authed: ApiAuthed, - Extension(db): Extension, - Extension(user_db): Extension, - Path(w_id): Path, - Json(draft): Json, -) -> Result<(StatusCode, String)> { - let authed = maybe_refresh_folders(&draft.path, &w_id, authed, &db).await; - - let mut tx = user_db.begin(&authed).await?; - - require_writer_of_path(&authed, &draft.path, &w_id, db, &draft.typ).await?; - - sqlx::query!( - "INSERT INTO draft - (workspace_id, path, value, typ) - VALUES ($1, $2, $3::text::json, $4) - ON CONFLICT (workspace_id, path, typ) - DO UPDATE SET value = EXCLUDED.value, created_at = now()", - &w_id, - draft.path, - //to preserve key orders - serde_json::to_string(&draft.value).unwrap(), - draft.typ as DraftType, - ) - .execute(&mut *tx) - .await?; - - tx.commit().await?; - - Ok((StatusCode::CREATED, format!("draft {} created", draft.path))) -} - -async fn delete_draft( - authed: ApiAuthed, - Extension(user_db): Extension, - Path((w_id, kind, path)): Path<(String, DraftType, StripPath)>, -) -> Result { - let mut tx = user_db.begin(&authed).await?; - - sqlx::query!( - "DELETE FROM draft WHERE path = $1 AND typ = $2 AND workspace_id = $3", - path.to_path(), - kind as DraftType, - w_id - ) - .execute(&mut *tx) - .await?; - tx.commit().await?; - - Ok(format!("deleted draft")) -} - -// async fn get_draft( -// authed: ApiAuthed, -// Extension(user_db): Extension, -// Path((w_id, path)): Path<(String, StripPath)>, -// ) -> JsonResult { -// let path = path.to_path(); -// let mut tx = user_db.begin(&authed).await?; - -// let script_o = sqlx::query_as!( -// Draft, -// r#"SELECT path, value, typ as "typ: DraftType" FROM draft WHERE path = $1 AND workspace_id = $2"#, -// path, -// w_id -// ) -// .fetch_optional(&mut *tx) -// .await?; -// tx.commit().await?; - -// let draft = not_found_if_none(script_o, "draft", path)?; -// Ok(Json(draft)) -// } diff --git a/backend/windmill-api/src/lib.rs b/backend/windmill-api/src/lib.rs index 41872d0353..474fef9410 100644 --- a/backend/windmill-api/src/lib.rs +++ b/backend/windmill-api/src/lib.rs @@ -78,7 +78,6 @@ mod concurrency_groups; mod db; mod db_health; -mod drafts; #[cfg(feature = "private")] pub mod ee; pub mod ee_oss; @@ -554,7 +553,6 @@ pub async fn run_server( concurrency_groups::workspaced_service(), ) .nest("/embeddings", embeddings::workspaced_service()) - .nest("/drafts", drafts::workspaced_service()) .nest("/favorites", favorite::workspaced_service()) .nest("/flows", flows::workspaced_service()) .nest( diff --git a/backend/windmill-api/src/workspaces_export.rs b/backend/windmill-api/src/workspaces_export.rs index 3f0cd3c646..d9321b3fe4 100644 --- a/backend/windmill-api/src/workspaces_export.rs +++ b/backend/windmill-api/src/workspaces_export.rs @@ -282,7 +282,6 @@ where "edited_by", "permissioned_as", "archived", - "has_draft", "error", "last_server_ping", "server_id", diff --git a/backend/windmill-types/src/flows.rs b/backend/windmill-types/src/flows.rs index 8b60eb0d44..7f9f16bf56 100644 --- a/backend/windmill-types/src/flows.rs +++ b/backend/windmill-types/src/flows.rs @@ -79,7 +79,6 @@ pub struct ListableFlow { pub archived: bool, pub extra_perms: serde_json::Value, pub starred: bool, - pub has_draft: bool, #[serde(skip_serializing_if = "Option::is_none")] pub draft_only: Option, #[serde(skip_serializing_if = "Option::is_none")] diff --git a/backend/windmill-types/src/scripts.rs b/backend/windmill-types/src/scripts.rs index e9e9337e6d..fd4f4b1be1 100644 --- a/backend/windmill-types/src/scripts.rs +++ b/backend/windmill-types/src/scripts.rs @@ -446,8 +446,6 @@ pub struct ListableScript { #[serde(skip_serializing_if = "Option::is_none")] pub description: Option, #[serde(skip_serializing_if = "Option::is_none")] - pub has_draft: Option, - #[serde(skip_serializing_if = "Option::is_none")] pub draft_only: Option, pub has_deploy_errors: bool, pub ws_error_handler_muted: Option, diff --git a/cli/package-lock.json b/cli/package-lock.json index f14fd32f8d..c502b3b25e 100644 --- a/cli/package-lock.json +++ b/cli/package-lock.json @@ -17,6 +17,7 @@ "jszip": "3.8.0", "minimatch": "^10.0.0", "open": "^10.0.0", + "pg-gateway": "0.3.0-beta.4", "svelte": "^5.45.2", "tar-stream": "^3.1.7", "windmill-parser-wasm-csharp": "1.510.1", @@ -1236,6 +1237,12 @@ "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==", "license": "(MIT AND Zlib)" }, + "node_modules/pg-gateway": { + "version": "0.3.0-beta.4", + "resolved": "https://registry.npmjs.org/pg-gateway/-/pg-gateway-0.3.0-beta.4.tgz", + "integrity": "sha512-CTjsM7Z+0Nx2/dyZ6r8zRsc3f9FScoD5UAOlfUx1Fdv/JOIWvRbF7gou6l6vP+uypXQVoYPgw8xZDXgMGvBa4Q==", + "license": "MIT" + }, "node_modules/process-nextick-args": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", diff --git a/frontend/src/lib/components/DiffDrawer.svelte b/frontend/src/lib/components/DiffDrawer.svelte index 0f89cfc3dd..58e245d74e 100644 --- a/frontend/src/lib/components/DiffDrawer.svelte +++ b/frontend/src/lib/components/DiffDrawer.svelte @@ -87,7 +87,7 @@ | { mode: 'normal' deployed: Value - draft: Value | undefined + draft?: Value | undefined current: Value defaultDiffType?: 'deployed' | 'draft' button?: { text: string; onClick: () => void } diff --git a/frontend/src/lib/components/DraftBadge.svelte b/frontend/src/lib/components/DraftBadge.svelte deleted file mode 100644 index a5bcf73df5..0000000000 --- a/frontend/src/lib/components/DraftBadge.svelte +++ /dev/null @@ -1,29 +0,0 @@ - - -{#if has_draft} - {#if draft_only} - - {#snippet text()} - Never deployed and is only a draft - {/snippet} - Draft only - - {:else} - - {#snippet text()} - Is deployed and has a draft - {/snippet} - +Draft - - {/if} -{/if} diff --git a/frontend/src/lib/components/FlowBuilder.svelte b/frontend/src/lib/components/FlowBuilder.svelte index 8c36de8b11..8becabc422 100644 --- a/frontend/src/lib/components/FlowBuilder.svelte +++ b/frontend/src/lib/components/FlowBuilder.svelte @@ -2,7 +2,6 @@ import { FlowService, type Flow, - DraftService, type PathScript, type OpenFlow, type InputTransform, @@ -13,7 +12,6 @@ import { initHistory, redo, undo } from '$lib/history.svelte' import { enterpriseLicense, userStore, workspaceStore, usedTriggerKinds } from '$lib/stores' import { - cleanValueProperties, generateRandomString, orderedJsonStringify, readFieldsRecursively, @@ -29,7 +27,7 @@ import DeployOverrideConfirmationModal from '$lib/components/common/confirmationModal/DeployOverrideConfirmationModal.svelte' import AIChangesWarningModal from '$lib/components/copilot/chat/flow/AIChangesWarningModal.svelte' - import { createRawSnippet, onMount, setContext, untrack } from 'svelte' + import { createRawSnippet, setContext, untrack } from 'svelte' import { writable } from 'svelte/store' import CenteredPage from './CenteredPage.svelte' import { Button } from './common' @@ -46,7 +44,6 @@ import { GroupEditor, setGroupEditorContext } from './graph/groupEditor.svelte' import { cleanFlow } from './flows/utils.svelte' import { - Save, DiffIcon, HistoryIcon, FileJson, @@ -77,12 +74,8 @@ import type { SavedAndModifiedValue } from './common/confirmationModal/unsavedTypes' import DeployButton from './DeployButton.svelte' import { invalidateWorkspacePaths } from './PathNameAutocomplete.svelte' - import type { FlowWithDraftAndDraftTriggers, Trigger } from './triggers/utils' - import { - deployTriggers, - filterDraftTriggers, - handleSelectTriggerFromKind - } from './triggers/utils' + import type { Trigger } from './triggers/utils' + import { deployTriggers, handleSelectTriggerFromKind } from './triggers/utils' import DraftTriggersConfirmationModal from './common/confirmationModal/DraftTriggersConfirmationModal.svelte' import { Triggers } from './triggers/triggers.svelte' import { StepsInputArgs } from './flows/stepsInputArgs.svelte' @@ -118,20 +111,15 @@ disabledFlowInputs = false, savedPrimarySchedule = undefined, version = undefined, - setSavedraftCb = undefined, draftTriggersFromUrl = undefined, selectedTriggerIndexFromUrl = undefined, children, loadedFromHistoryFromUrl, noInitial = false, liveEditorDraftStoragePath = undefined, - onSaveInitial, - onSaveDraft, onDeploy, onDeployError, onDetails, - onSaveDraftError, - onSaveDraftOnlyAtNewPath, onHistoryRestore, onNavigate }: FlowBuilderProps = $props() @@ -261,128 +249,9 @@ } let loadingSave = $state(false) - let loadingDraft = $state(false) - export async function saveDraft(forceSave = false): Promise { - withAIChangesWarning(async () => { - await saveDraftInternal(forceSave) - }) - } - - async function saveDraftInternal(forceSave = false): Promise { - if (!newFlow && !savedFlow) { - return - } - - if (savedFlow) { - const draftOrDeployed = cleanValueProperties(savedFlow.draft || savedFlow) - const currentDraftTriggers = structuredClone(triggersState.getDraftTriggersSnapshot()) - const current = cleanValueProperties( - $state.snapshot({ - ...flowStore.val, - path: $pathStore, - draft_triggers: currentDraftTriggers - }) - ) - if (!forceSave && orderedJsonStringify(draftOrDeployed) === orderedJsonStringify(current)) { - sendUserToast('No changes detected, ignoring', false, [ - { - label: 'Save anyway', - callback: () => { - saveDraftInternal(true) - } - } - ]) - return - } - } - loadingDraft = true - try { - const flow = cleanFlow(flowStore.val) - if (newFlow || savedFlow?.draft_only) { - if (savedFlow?.draft_only) { - await FlowService.deleteFlowByPath({ - workspace: $workspaceStore!, - path: initialPath, - keepCaptures: true - }) - } - if (!initialPath || $pathStore != initialPath) { - await CaptureService.moveCapturesAndConfigs({ - workspace: $workspaceStore!, - path: initialPath || fakeInitialPath, - requestBody: { - new_path: $pathStore - }, - runnableKind: 'flow' - }) - } - await FlowService.createFlow({ - workspace: $workspaceStore!, - requestBody: { - path: $pathStore, - summary: flow.summary ?? '', - description: flow.description ?? '', - value: flow.value, - schema: flow.schema, - tag: flow.tag, - draft_only: true, - ws_error_handler_muted: flow.ws_error_handler_muted, - visible_to_runner_only: flow.visible_to_runner_only, - on_behalf_of_email: flow.on_behalf_of_email, - labels: (flow as any).labels - } - }) - } - await DraftService.createDraft({ - workspace: $workspaceStore!, - requestBody: { - path: newFlow || savedFlow?.draft_only ? $pathStore : initialPath, - typ: 'flow', - value: { - ...flow, - path: $pathStore, - draft_triggers: triggersState.getDraftTriggersSnapshot() - } - } - }) - - savedFlow = { - ...(newFlow || savedFlow?.draft_only - ? { - ...structuredClone($state.snapshot(flowStore.val)), - path: $pathStore, - draft_only: true - } - : savedFlow), - draft: { - ...structuredClone($state.snapshot(flowStore.val)), - path: $pathStore, - draft_triggers: structuredClone(triggersState.getDraftTriggersSnapshot()) - } - } as FlowWithDraftAndDraftTriggers - - let savedAtNewPath = false - if (newFlow) { - onSaveInitial?.({ path: $pathStore, id: getSelectedId() ?? 'settings' }) - } else if (savedFlow?.draft_only && $pathStore !== initialPath) { - savedAtNewPath = true - initialPath = $pathStore - onSaveDraftOnlyAtNewPath?.({ path: $pathStore, selectedId: getSelectedId() ?? 'settings' }) - // this is so we can use the flow builder outside of sveltekit - } - onSaveDraft?.({ path: $pathStore, savedAtNewPath, newFlow }) - sendUserToast('Saved as draft') - } catch (error) { - sendUserToast(`Error while saving the flow as a draft: ${error.body || error.message}`, true) - onSaveDraftError?.({ error }) - } - loadingDraft = false - } - - onMount(() => { - setSavedraftCb?.(() => saveDraft()) - }) + // No-op: persistence happens via the page-level UserDraft autosave. + export function saveDraft(): void {} export function computeUnlockedSteps(flow: Flow) { return Object.fromEntries( @@ -677,7 +546,7 @@ [ { type: 'webhook', path: '', isDraft: false }, { type: 'default_email', path: '', isDraft: false }, - ...(untrack(() => draftTriggersFromUrl) ?? savedFlow?.draft?.draft_triggers ?? []) + ...(untrack(() => draftTriggersFromUrl) ?? []) ], untrack(() => selectedTriggerIndexFromUrl) ) @@ -706,10 +575,6 @@ $primaryScheduleStore, $userStore ) - - if (savedFlow && savedFlow.draft) { - savedFlow = filterDraftTriggers(savedFlow, triggersState) as FlowWithDraftAndDraftTriggers - } } function handleUndo() { @@ -834,7 +699,6 @@ diffDrawer?.setDiff({ mode: 'normal', deployed: deployedValue ?? savedFlow, - draft: savedFlow?.draft, current: { ...currentFlow, path: $pathStore, @@ -879,25 +743,7 @@ const mod = isMac() ? '⌘' : 'Ctrl+' function getMoreItems(): Item[] { - // When the top bar is compact, fold the inline Diff + Save draft buttons - // in here so they stay reachable. Save draft keeps its keyboard shortcut. - const compactExtras: Item[] = compactTopbar - ? [ - ...(customUi?.topBar?.draft !== false - ? [ - { - displayName: 'Save draft', - icon: Save, - action: () => saveDraft(), - shortcut: `${mod}S`, - disabled: (!newFlow && !savedFlow) || loading - } - ] - : []) - ] - : [] return [ - ...compactExtras, ...baseMenuItems, { displayName: 'Undo', @@ -905,7 +751,7 @@ action: () => handleUndo(), disabled: $history.index === 0, shortcut: `${mod}Z`, - separatorTop: compactExtras.length > 0 || baseMenuItems.length > 0 + separatorTop: baseMenuItems.length > 0 }, { displayName: 'Redo', @@ -1018,17 +864,7 @@ ] } - function handleDeployTrigger(trigger: Trigger) { - const { id, path, type } = trigger - //Update the saved flow to remove the draft trigger that is deployed - if (savedFlow && savedFlow.draft && savedFlow.draft.draft_triggers) { - const newSavedDraftTrigers = savedFlow.draft.draft_triggers.filter( - (t) => t.id !== id || t.path !== path || t.type !== type - ) - savedFlow.draft.draft_triggers = - newSavedDraftTrigers.length > 0 ? newSavedDraftTrigers : undefined - } - } + function handleDeployTrigger(_trigger: Trigger) {} let forceTestTab: Record = $state({}) let highlightArg: Record = $state({}) @@ -1218,19 +1054,6 @@ {#if !compactTopbar} {@render previewButtons()} {/if} - {#if customUi?.topBar?.draft !== false && !compactTopbar} - - {/if} await handleSaveFlow(detail)} diff --git a/frontend/src/lib/components/ScriptBuilder.svelte b/frontend/src/lib/components/ScriptBuilder.svelte index 710ce9bd5d..542bf81cdc 100644 --- a/frontend/src/lib/components/ScriptBuilder.svelte +++ b/frontend/src/lib/components/ScriptBuilder.svelte @@ -3,10 +3,9 @@ const bubble = createBubbler() import { - DraftService, ScriptService, - type NewScriptWithDraft, type Script, + type NewScript, type TriggersCount, PostgresTriggerService, CaptureService, @@ -31,7 +30,6 @@ workspaceStore } from '$lib/stores' import { - cleanValueProperties, emptySchema, emptyString, generateRandomString, @@ -58,14 +56,13 @@ EllipsisVertical, Plus, Rocket, - Save, Settings, Shuffle, Tag, X } from 'lucide-svelte' import DropdownV2 from './DropdownV2.svelte' - import { isMac, type Item } from '$lib/utils' + import { type Item } from '$lib/utils' import { sendUserToast } from '$lib/toast' import { isCloudHosted } from '$lib/cloud' import Awareness from './Awareness.svelte' @@ -91,13 +88,7 @@ import CaptureTable from './triggers/CaptureTable.svelte' import type { SavedAndModifiedValue } from './common/confirmationModal/unsavedTypes' import DeployButton from './DeployButton.svelte' - import { - type NewScriptWithDraftAndDraftTriggers, - type Trigger, - deployTriggers, - filterDraftTriggers, - handleSelectTriggerFromKind - } from './triggers/utils' + import { type Trigger, deployTriggers, handleSelectTriggerFromKind } from './triggers/utils' import DraftTriggersConfirmationModal from './common/confirmationModal/DraftTriggersConfirmationModal.svelte' import { Triggers } from './triggers/triggers.svelte' import type { ScriptBuilderProps } from './script_builder' @@ -129,10 +120,7 @@ children, onDeploy, onDeployError, - onSaveInitial, onSeeDetails, - onSaveDraftError, - onSaveDraft, onNavigate, disableAi }: ScriptBuilderProps = $props() @@ -163,18 +151,10 @@ // Top-bar responsive collapse — container width, not viewport. let topbarWidth = $state(0) const compactTopbar = $derived(topbarWidth > 0 && topbarWidth < 720) - const mod = isMac() ? '⌘' : 'Ctrl+' function getCompactMenuItems(): Item[] { const hasTags = ($workerTags?.length ?? 0) > 0 return [ - { - displayName: 'Save draft', - icon: Save, - action: () => saveDraft(), - shortcut: `${mod}S`, - disabled: initialPath != '' && !savedScript - }, ...(customUi?.topBar?.tagEdit != false && hasTags ? [ { @@ -289,13 +269,6 @@ $primaryScheduleStore, $userStore ) - - if (savedScript && savedScript.draft && savedScript.draft.draft_triggers) { - savedScript = filterDraftTriggers( - savedScript, - triggersState - ) as NewScriptWithDraftAndDraftTriggers - } } // Add triggers context store @@ -366,7 +339,6 @@ let pathError = $state('') let loadingSave = $state(false) - let loadingDraft = $state(false) if (script.content == '') { if (template === 'wac_python') { @@ -620,7 +592,7 @@ } const { draft_triggers: _, ...newScript } = structuredClone($state.snapshot(script)) - savedScript = structuredClone($state.snapshot(newScript)) as NewScriptWithDraft + savedScript = structuredClone($state.snapshot(newScript)) setDraftTriggers([]) if (!disableHistoryChange) { @@ -644,158 +616,12 @@ loadingSave = false } - async function saveDraft(forceSave = false): Promise { - scriptEditor?.flushModuleState() - if (initialPath != '' && !savedScript) { - return - } - - if (savedScript) { - const draftOrDeployed = cleanValueProperties(savedScript.draft || savedScript) - const currentTriggers = structuredClone(triggersState.getDraftTriggersSnapshot()) - const current = cleanValueProperties({ ...script, draft_triggers: currentTriggers }) - if (!forceSave && orderedJsonStringify(draftOrDeployed) === orderedJsonStringify(current)) { - sendUserToast('No changes detected, ignoring', false, [ - { - label: 'Save anyway', - callback: () => { - saveDraft(true) - } - } - ]) - return - } - } - - loadingDraft = true - try { - script.schema = script.schema ?? emptySchema() - try { - const result = await inferArgs( - script.language, - script.content, - script.schema as any, - script.kind === 'preprocessor' ? 'preprocessor' : undefined - ) - if (script.kind === 'preprocessor') { - script.auto_kind = undefined - script.has_preprocessor = undefined - } else { - script.auto_kind = result?.auto_kind || undefined - script.has_preprocessor = result?.has_preprocessor || undefined - } - } catch (error) { - sendUserToast(`Could not parse code, are you sure it is valid?`, true) - } - let newHash = '' - if (initialPath == '' || savedScript?.draft_only) { - if (savedScript?.draft_only) { - await ScriptService.deleteScriptByPath({ - workspace: $workspaceStore!, - path: initialPath, - keepCaptures: true - }) - script.parent_hash = undefined - } - if (!initialPath || script.path != initialPath) { - await CaptureService.moveCapturesAndConfigs({ - workspace: $workspaceStore!, - path: initialPath || fakeInitialPath, - requestBody: { - new_path: script.path - }, - runnableKind: 'script' - }) - } - newHash = await ScriptService.createScript({ - workspace: $workspaceStore!, - requestBody: { - path: script.path, - summary: script.summary, - description: script.description ?? '', - content: script.content, - schema: script.schema, - is_template: script.is_template, - language: script.language, - kind: script.kind, - tag: script.tag, - draft_only: true, - envs: script.envs, - concurrent_limit: script.concurrent_limit, - concurrency_time_window_s: script.concurrency_time_window_s, - debounce_key: emptyString(script.debounce_key) ? undefined : script.debounce_key, - debounce_delay_s: script.debounce_delay_s, - debounce_args_to_accumulate: - script.debounce_args_to_accumulate && script.debounce_args_to_accumulate.length > 0 - ? script.debounce_args_to_accumulate - : undefined, - max_total_debouncing_time: script.max_total_debouncing_time, - max_total_debounces_amount: script.max_total_debounces_amount, - cache_ttl: script.cache_ttl, - cache_ignore_s3_path: script.cache_ignore_s3_path, - ws_error_handler_muted: script.ws_error_handler_muted, - priority: script.priority, - restart_unless_cancelled: script.restart_unless_cancelled, - timeout: script.timeout, - concurrency_key: emptyString(script.concurrency_key) - ? undefined - : script.concurrency_key, - visible_to_runner_only: script.visible_to_runner_only, - auto_kind: script.auto_kind, - has_preprocessor: script.has_preprocessor, - on_behalf_of_email: script.on_behalf_of_email, - assets: script.assets, - modules: script.modules, - labels: script.labels - } - }) - } - const draftTriggers = triggersState.getDraftTriggersSnapshot() - await DraftService.createDraft({ - workspace: $workspaceStore!, - requestBody: { - path: initialPath == '' || savedScript?.draft_only ? script.path : initialPath, - typ: 'script', - value: { - ...script, - draft_triggers: draftTriggers - } - } - }) - - const clonedScript = structuredClone($state.snapshot(script)) - savedScript = { - ...(initialPath == '' || savedScript?.draft_only - ? { ...clonedScript, draft_only: true } - : savedScript), - draft: { - ...clonedScript, - draft_triggers: draftTriggers - } - } as NewScriptWithDraftAndDraftTriggers - - let savedAtNewPath = false - if (initialPath == '' || (savedScript?.draft_only && script.path !== initialPath)) { - savedAtNewPath = true - initialPath = script.path - onSaveInitial?.({ path: script.path, hash: newHash }) - } - onSaveDraft?.({ path: script.path, savedAtNewPath, script }) - - sendUserToast('Saved as draft') - } catch (error) { - sendUserToast( - `Error while saving the script as a draft: ${error.body || error.message}`, - true - ) - onSaveDraftError?.({ path: script.path, error }) - } - loadingDraft = false - } + // No-op: persistence happens via the page-level UserDraft autosave. + function saveDraft(): void {} function computeDropdownItems( initialPath: string, - savedScript: NewScriptWithDraftAndDraftTriggers | undefined, + savedScript: Script | NewScript | undefined, diffDrawer: DiffDrawerI | undefined ) { let dropdownItems: { label: string; onClick: () => void }[] = @@ -972,17 +798,7 @@ } } - function handleDeployTrigger(trigger: Trigger) { - const { id, path, type } = trigger - //Update the saved script to remove the draft trigger that is deployed - if (savedScript && savedScript.draft && savedScript.draft.draft_triggers) { - const newSavedDraftTrigers = savedScript.draft.draft_triggers.filter( - (t) => t.id !== id || t.path !== path || t.type !== type - ) - savedScript.draft.draft_triggers = - newSavedDraftTrigers.length > 0 ? newSavedDraftTrigers : undefined - } - } + function handleDeployTrigger(_trigger: Trigger) {} function onScriptLanguageTrigger(lang: 'docker' | 'bunnative' | ScriptLang) { if (lang == 'docker') { @@ -2035,17 +1851,6 @@ {/if} {/if} {@render settingsButton()} - {/if} fromHub)) let historyBrowserDrawerOpen = $state(false) @@ -198,10 +188,6 @@ saveDrawerOpen = false } - function closeDraftDrawer() { - draftDrawerOpen = false - } - async function createApp(path: string) { policy = await updatePolicy($app, policy) try { @@ -257,7 +243,7 @@ replaceFalseWithUndefined({ summary: $summary, value: $app, - path: newEditedPath || savedApp.draft?.path || savedApp.path, + path: newEditedPath || savedApp.path, policy, custom_path: customPath }) @@ -358,156 +344,6 @@ return } - async function saveInitialDraft() { - policy = await updatePolicy($app, policy) - try { - await AppService.createApp({ - workspace: $workspaceStore!, - requestBody: { - value: $app, - path: newEditedPath, - summary: $summary, - policy, - draft_only: true, - custom_path: customPath - } - }) - await DraftService.createDraft({ - workspace: $workspaceStore!, - requestBody: { - path: newEditedPath, - typ: 'app', - value: { - value: $app, - path: newEditedPath, - summary: $summary, - policy, - custom_path: customPath - } - } - }) - savedApp = { - summary: $summary, - value: structuredClone($state.snapshot($app)), - path: newEditedPath, - policy, - draft_only: true, - draft: { - summary: $summary, - value: structuredClone($state.snapshot($app)), - path: newEditedPath, - policy, - custom_path: customPath - }, - custom_path: customPath - } - - draftDrawerOpen = false - // The initial draft was promoted to a real path on the backend — - // drop the autosave keyed on the prior (possibly empty) path so - // a future "+ App" click opens on a clean slate. - UserDraft.remove('app', $appPath) - onSavedNewAppPath?.(newEditedPath) - } catch (e) { - sendUserToast('Error saving initial draft', e) - } - draftDrawerOpen = false - } - - async function saveDraft(forceSave = false) { - if (newApp) { - // initial draft - draftDrawerOpen = true - return - } - if (!savedApp) { - return - } - const draftOrDeployed = cleanValueProperties(savedApp.draft || savedApp) - const current = cleanValueProperties({ - summary: $summary, - value: $app, - path: newEditedPath || savedApp.draft?.path || savedApp.path, - policy - }) - if (!forceSave && orderedJsonStringify(draftOrDeployed) === orderedJsonStringify(current)) { - sendUserToast('No changes detected, ignoring', false, [ - { - label: 'Save anyway', - callback: () => { - saveDraft(true) - } - } - ]) - return - } - loading.saveDraft = true - try { - policy = await updatePolicy($app, policy) - let path = $appPath - if (savedApp.draft_only) { - await AppService.deleteApp({ - workspace: $workspaceStore!, - path: path - }) - await AppService.createApp({ - workspace: $workspaceStore!, - requestBody: { - value: $app!, - summary: $summary, - policy, - path: newEditedPath || path, - draft_only: true, - custom_path: customPath - } - }) - } - await DraftService.createDraft({ - workspace: $workspaceStore!, - requestBody: { - path: savedApp.draft_only ? newEditedPath || path : path, - typ: 'app', - value: { - value: $app!, - summary: $summary, - policy, - path: newEditedPath || path - } - } - }) - - savedApp = { - ...(savedApp?.draft_only - ? { - summary: $summary, - value: structuredClone($state.snapshot($app)), - path: savedApp.draft_only ? newEditedPath || path : path, - policy, - draft_only: true, - custom_path: customPath - } - : savedApp), - draft: { - summary: $summary, - value: structuredClone($state.snapshot($app)), - path: newEditedPath || path, - policy, - custom_path: customPath - } - } - - sendUserToast('Draft saved') - UserDraft.remove('app', path) - loading.saveDraft = false - if (newApp || savedApp.draft_only) { - onSavedNewAppPath?.(newEditedPath || path) - } - } catch (e) { - loading.saveDraft = false - throw e - } - } - let onLatest = $state(true) async function compareVersions() { if (version === undefined) { @@ -553,7 +389,6 @@ break case 's': if (event.ctrlKey || event.metaKey) { - saveDraft() event.preventDefault() } break @@ -597,13 +432,6 @@ let moreItems = $derived([ ...(compactTopbar ? [ - { - displayName: 'Save draft', - icon: Save, - action: () => saveDraft(), - shortcut: `${mod}S`, - disabled: !newApp && !savedApp - }, { displayName: `Debug runs (${$jobs?.length > 99 ? '99+' : ($jobs?.length ?? 0)})`, icon: Bug, @@ -680,7 +508,7 @@ action: () => { appReportingDrawerOpen = true }, - disabled: !savedApp || savedApp.draft_only + disabled: !savedApp }, { displayName: 'Diff', @@ -697,11 +525,10 @@ diffDrawer?.setDiff({ mode: 'normal', deployed: deployedValue ?? savedApp, - draft: savedApp.draft, current: { summary: $summary, value: $app, - path: newEditedPath || savedApp.draft?.path || savedApp.path, + path: newEditedPath || savedApp.path, policy, custom_path: customPath } @@ -825,7 +652,7 @@ modifiedValue: { summary: $summary, value: $app, - path: newEditedPath || savedApp?.draft?.path || savedApp?.path, + path: newEditedPath || savedApp?.path, policy, custom_path: customPath } @@ -841,39 +668,12 @@ currentValue={{ summary: $summary, value: $app, - path: newEditedPath || savedApp?.draft?.path || savedApp?.path, + path: newEditedPath || savedApp?.path, policy, custom_path: customPath }} /> -{#if $appPath == ''} - - closeDraftDrawer()}> - {#snippet actions()} -
- -
- {/snippet} - - -
-
-{/if} - - {/if} diff --git a/frontend/src/lib/components/common/table/AppRow.svelte b/frontend/src/lib/components/common/table/AppRow.svelte index 1c0c5b08d1..62e82c447d 100644 --- a/frontend/src/lib/components/common/table/AppRow.svelte +++ b/frontend/src/lib/components/common/table/AppRow.svelte @@ -4,12 +4,11 @@ import type MoveDrawer from '$lib/components/MoveDrawer.svelte' import SharedBadge from '$lib/components/SharedBadge.svelte' import type ShareModal from '$lib/components/ShareModal.svelte' - import { AppService, DraftService, type ListableApp } from '$lib/gen' + import { AppService, type ListableApp } from '$lib/gen' import { userStore, workspaceStore } from '$lib/stores' import { createEventDispatcher } from 'svelte' import Button from '../button/Button.svelte' import Row from './Row.svelte' - import DraftBadge from '$lib/components/DraftBadge.svelte' import Badge from '../badge/Badge.svelte' import { ExternalLink, @@ -28,7 +27,7 @@ import { goto as gotoUrl } from '$app/navigation' import { page } from '$app/state' import type DeployWorkspaceDrawer from '$lib/components/DeployWorkspaceDrawer.svelte' - import { DELETE, copyToClipboard } from '$lib/utils' + import { copyToClipboard } from '$lib/utils' import AppDeploymentHistory from '$lib/components/apps/editor/AppDeploymentHistory.svelte' import { isDeployable } from '$lib/utils_deployable' import { getDeployUiSettings } from '$lib/components/home/deploy_ui' @@ -37,7 +36,7 @@ import { isCloudHosted } from '$lib/cloud' interface Props { - app: ListableApp & { has_draft?: boolean; draft_only?: boolean; canWrite: boolean } + app: ListableApp & { draft_only?: boolean; canWrite: boolean } marked: string | undefined shareModal: ShareModal moveDrawer: MoveDrawer @@ -98,7 +97,6 @@ Raw {/if} - {#if app.labels?.length}
{#each app.labels.slice(0, 3) as label} @@ -155,7 +153,7 @@ aiId={`app-row-dropdown-${app.summary?.length > 0 ? app.summary : app.path}`} aiDescription={`Open dropdown for app ${app.summary?.length > 0 ? app.summary : app.path} options`} items={async () => { - let { draft_only, canWrite, summary, execution_mode, path, has_draft } = app + let { draft_only, canWrite, summary, execution_mode, path } = app const canEdit = canWrite && showEditButton if (draft_only) { @@ -271,25 +269,6 @@ } ] : []), - ...(has_draft - ? [ - { - displayName: 'Delete Draft', - icon: Trash, - action: async () => { - await DraftService.deleteDraft({ - workspace: $workspaceStore ?? '', - path, - kind: 'app' - }) - dispatch('change') - }, - type: DELETE, - disabled: !canWrite, - hide: $userStore?.operator - } - ] - : []), { displayName: 'Delete', icon: Trash, diff --git a/frontend/src/lib/components/common/table/FlowRow.svelte b/frontend/src/lib/components/common/table/FlowRow.svelte index 14ee1a3287..763c9c33ba 100644 --- a/frontend/src/lib/components/common/table/FlowRow.svelte +++ b/frontend/src/lib/components/common/table/FlowRow.svelte @@ -6,15 +6,14 @@ import ScheduleEditor from '$lib/components/triggers/schedules/ScheduleEditor.svelte' import SharedBadge from '$lib/components/SharedBadge.svelte' import type ShareModal from '$lib/components/ShareModal.svelte' - import { FlowService, type Flow, DraftService } from '$lib/gen' + import { FlowService, type Flow } from '$lib/gen' import { userStore, workspaceStore } from '$lib/stores' import { createEventDispatcher } from 'svelte' import Badge from '../badge/Badge.svelte' import Button from '../button/Button.svelte' import Row from './Row.svelte' - import DraftBadge from '$lib/components/DraftBadge.svelte' import { sendUserToast } from '$lib/toast' - import { DELETE, copyToClipboard, isOwner } from '$lib/utils' + import { copyToClipboard, isOwner } from '$lib/utils' import { isDeployable } from '$lib/utils_deployable' import type DeployWorkspaceDrawer from '$lib/components/DeployWorkspaceDrawer.svelte' @@ -39,7 +38,7 @@ import { isCloudHosted } from '$lib/cloud' interface Props { - flow: Flow & { has_draft?: boolean; draft_only?: boolean; canWrite: boolean } + flow: Flow & { draft_only?: boolean; canWrite: boolean } marked: string | undefined shareModal: ShareModal moveDrawer: MoveDrawer @@ -121,7 +120,6 @@ archived {/if} - {#if flow.labels?.length}
{#each flow.labels.slice(0, 3) as label} @@ -180,7 +178,7 @@ aiId={`flow-row-dropdown-${flow.summary?.length > 0 ? flow.summary : flow.path}`} aiDescription={`Open dropdown for flow ${flow.summary?.length > 0 ? flow.summary : flow.path} options`} items={async () => { - let { draft_only, path, archived, has_draft } = flow + let { draft_only, path, archived } = flow let owner = isOwner(path, $userStore, $workspaceStore) const canEdit = flow.canWrite && showEditButton if (draft_only) { @@ -293,25 +291,6 @@ disabled: !owner || !canEdit, hide: $userStore?.operator }, - ...(has_draft - ? [ - { - displayName: 'Delete Draft', - icon: Trash, - action: async () => { - await DraftService.deleteDraft({ - workspace: $workspaceStore ?? '', - path, - kind: 'flow' - }) - dispatch('change') - }, - type: DELETE, - disabled: !owner, - hide: $userStore?.operator - } - ] - : []), { displayName: 'Delete', icon: Trash, diff --git a/frontend/src/lib/components/common/table/ScriptRow.svelte b/frontend/src/lib/components/common/table/ScriptRow.svelte index 3d0aec796b..75ae4e36ab 100644 --- a/frontend/src/lib/components/common/table/ScriptRow.svelte +++ b/frontend/src/lib/components/common/table/ScriptRow.svelte @@ -7,16 +7,15 @@ import SharedBadge from '$lib/components/SharedBadge.svelte' import type ShareModal from '$lib/components/ShareModal.svelte' - import { ScriptService, type Script, DraftService } from '$lib/gen' + import { ScriptService, type Script } from '$lib/gen' import { hubBaseUrlStore, userStore, workspaceStore } from '$lib/stores' import { createEventDispatcher } from 'svelte' import Badge from '../badge/Badge.svelte' import Button from '../button/Button.svelte' import Row from './Row.svelte' - import DraftBadge from '$lib/components/DraftBadge.svelte' import { sendUserToast } from '$lib/toast' - import { capitalize, copyToClipboard, DELETE, isOwner } from '$lib/utils' + import { capitalize, copyToClipboard, isOwner } from '$lib/utils' import { isDeployable } from '$lib/utils_deployable' import type DeployWorkspaceDrawer from '$lib/components/DeployWorkspaceDrawer.svelte' @@ -168,7 +167,6 @@ > {/if} - {#if script.labels?.length}
{#each script.labels.slice(0, 3) as label} @@ -404,25 +402,6 @@ hide: $userStore?.operator }, - ...(script.has_draft - ? [ - { - displayName: 'Delete Draft', - icon: Trash, - action: async () => { - await DraftService.deleteDraft({ - workspace: $workspaceStore ?? '', - path: script.path, - kind: 'script' - }) - dispatch('change') - }, - type: DELETE, - disabled: !owner, - hide: $userStore?.operator - } - ] - : []), ...($userStore?.is_admin || $userStore?.is_super_admin ? [ { diff --git a/frontend/src/lib/components/copilot/chat/global/core.test.ts b/frontend/src/lib/components/copilot/chat/global/core.test.ts index 6a7a9c8a9c..ddc7e88a29 100644 --- a/frontend/src/lib/components/copilot/chat/global/core.test.ts +++ b/frontend/src/lib/components/copilot/chat/global/core.test.ts @@ -45,8 +45,8 @@ vi.mock('$lib/gen', async () => { ScriptService: wrapService(actual.ScriptService, { existsScriptByPath: vi.fn(async () => false), createScript: vi.fn(async () => 'created'), - getScriptByPathWithDraft: vi.fn(async () => { - throw new Error('getScriptByPathWithDraft mock not configured') + getScriptByPath: vi.fn(async () => { + throw new Error('getScriptByPath mock not configured') }), listScripts: vi.fn(async () => []) }), @@ -57,9 +57,6 @@ vi.mock('$lib/gen', async () => { getFlowByPath: vi.fn(async () => { throw new Error('getFlowByPath mock not configured') }), - getFlowByPathWithDraft: vi.fn(async () => { - throw new Error('getFlowByPathWithDraft mock not configured') - }), getFlowLatestVersion: vi.fn(async () => ({ id: 1 })), listFlows: vi.fn(async () => []) }), @@ -77,8 +74,8 @@ vi.mock('$lib/gen', async () => { }), AppService: wrapService(actual.AppService, { existsApp: vi.fn(async () => false), - getAppByPathWithDraft: vi.fn(async () => { - throw new Error('getAppByPathWithDraft mock not configured') + getAppByPath: vi.fn(async () => { + throw new Error('getAppByPath mock not configured') }), listApps: vi.fn(async () => []) }), @@ -537,23 +534,14 @@ describe('global AI tools', () => { it('preserves existing script metadata and seeds freshness on first script write', async () => { vi.mocked(ScriptService.existsScriptByPath).mockResolvedValueOnce(true) - vi.mocked(ScriptService.getScriptByPathWithDraft).mockResolvedValueOnce({ + vi.mocked(ScriptService.getScriptByPath).mockResolvedValueOnce({ path: 'f/scripts/existing', hash: 'deployed-hash', - draft_created_at: '2026-05-22T10:00:00Z', summary: 'deployed summary', description: 'deployed description', content: 'old deployed content', language: 'bun', - kind: 'script', - draft: { - path: 'f/scripts/existing', - summary: 'db draft summary', - description: 'db draft description', - content: 'old draft content', - language: 'bun', - kind: 'script' - } + kind: 'script' } as any) await callGlobalTool('write_script', { @@ -569,20 +557,19 @@ describe('global AI tools', () => { path: 'f/scripts/existing', parent_hash: 'deployed-hash', summary: 'new summary', - description: 'db draft description', + description: 'deployed description', content: 'new content', language: 'bun' }) expect(UserDraft.getMeta('script', 'f/scripts/existing', { workspace: WORKSPACE })).toEqual({ - remoteRev: 'deployed-hash', - remoteDraftRev: '2026-05-22T10:00:00Z' + remoteRev: 'deployed-hash' }) }) it('preserves existing flow metadata and seeds freshness on first flow write', async () => { vi.mocked(FlowService.existsFlowByPath).mockResolvedValueOnce(true) vi.mocked(FlowService.getFlowLatestVersion).mockResolvedValueOnce({ id: 42 } as any) - vi.mocked(FlowService.getFlowByPathWithDraft).mockResolvedValueOnce({ + vi.mocked(FlowService.getFlowByPath).mockResolvedValueOnce({ path: 'f/flows/existing', summary: 'deployed summary', description: 'deployed description', @@ -591,19 +578,7 @@ describe('global AI tools', () => { edited_by: 'admin', edited_at: '2026-05-22T09:00:00Z', archived: false, - extra_perms: {}, - draft_created_at: '2026-05-22T10:00:00Z', - draft: { - path: 'f/flows/existing', - summary: 'db draft summary', - description: 'db draft description', - value: { modules: [] }, - schema: { properties: { draft: { type: 'string' } } }, - edited_by: 'admin', - edited_at: '2026-05-22T09:30:00Z', - archived: false, - extra_perms: {} - } + extra_perms: {} } as any) await callGlobalTool('write_flow', { @@ -615,12 +590,11 @@ describe('global AI tools', () => { expect(UserDraft.get('flow', 'f/flows/existing', { workspace: WORKSPACE })).toMatchObject({ path: 'f/flows/existing', summary: 'new summary', - description: 'db draft description', + description: 'deployed description', value: { modules: [{ id: 'step', value: { type: 'identity' } }] } }) expect(UserDraft.getMeta('flow', 'f/flows/existing', { workspace: WORKSPACE })).toEqual({ - remoteRev: 42, - remoteDraftRev: '2026-05-22T10:00:00Z' + remoteRev: 42 }) }) @@ -733,32 +707,22 @@ describe('global AI tools', () => { }) it('seeds raw app draft metadata on first app write', async () => { - vi.mocked(AppService.getAppByPathWithDraft).mockResolvedValueOnce({ + vi.mocked(AppService.getAppByPath).mockResolvedValueOnce({ path: 'f/apps/report', summary: 'deployed app', versions: [3, 4], - draft_created_at: '2026-05-22T10:30:00Z', value: { files: { '/src/App.tsx': 'deployed content' }, - runnables: {}, - data: { tables: [] } + runnables: { + main: { + type: 'inline', + inlineScript: { language: 'bun', content: 'export async function main() {}' } + } + }, + data: { tables: ['orders'], datatable: 'db', schema: 'public' } }, policy: { execution_mode: 'publisher' }, - custom_path: 'report', - draft: { - summary: 'saved app draft', - value: { - files: { '/src/App.tsx': 'draft content' }, - runnables: { - main: { - type: 'inline', - inlineScript: { language: 'bun', content: 'export async function main() {}' } - } - }, - data: { tables: ['orders'], datatable: 'db', schema: 'public' } - }, - policy: { execution_mode: 'anonymous' } - } + custom_path: 'report' } as any) await callGlobalTool('write_app_file', { @@ -769,9 +733,9 @@ describe('global AI tools', () => { const draft = UserDraft.get('raw_app', 'f/apps/report', { workspace: WORKSPACE }) expect(draft).toMatchObject({ - summary: 'saved app draft', + summary: 'deployed app', files: { - '/src/App.tsx': 'draft content', + '/src/App.tsx': 'deployed content', '/src/New.tsx': 'export default function New() { return null }' }, runnables: { @@ -781,12 +745,11 @@ describe('global AI tools', () => { } }, data: { tables: ['orders'], datatable: 'db', schema: 'public' }, - policy: { execution_mode: 'anonymous' }, + policy: { execution_mode: 'publisher' }, custom_path: 'report' }) expect(UserDraft.getMeta('raw_app', 'f/apps/report', { workspace: WORKSPACE })).toEqual({ - remoteRev: 4, - remoteDraftRev: '2026-05-22T10:30:00Z' + remoteRev: 4 }) }) @@ -841,39 +804,31 @@ describe('global AI tools', () => { expect(item.value.backend[0]).not.toHaveProperty('content') }) - it('summarizes backend raw app drafts from the same source as file reads', async () => { - const appWithDraft = { + it('summarizes backend raw apps from the same source as file reads', async () => { + const deployedApp = { path: 'f/apps/report', summary: 'deployed app', versions: [5], value: { - files: { '/src/App.tsx': 'deployed content' }, - runnables: {}, - data: { tables: ['deployed'] } - }, - draft: { - summary: 'saved app draft', - value: { - files: { - '/src/App.tsx': 'draft content', - '/src/DraftOnly.tsx': 'draft-only content' - }, - runnables: { - main: { - type: 'inline', - inlineScript: { - language: 'bun', - content: 'export async function main() { return "draft" }' - } + files: { + '/src/App.tsx': 'deployed content', + '/src/Helper.tsx': 'helper content' + }, + runnables: { + main: { + type: 'inline', + inlineScript: { + language: 'bun', + content: 'export async function main() { return "deployed" }' } - }, - data: { tables: ['draft'] } - } + } + }, + data: { tables: ['deployed'] } } } - vi.mocked(AppService.getAppByPathWithDraft) - .mockResolvedValueOnce(appWithDraft as any) - .mockResolvedValueOnce(appWithDraft as any) + vi.mocked(AppService.getAppByPath) + .mockResolvedValueOnce(deployedApp as any) + .mockResolvedValueOnce(deployedApp as any) const raw = await callGlobalTool('read_workspace_item', { type: 'app', @@ -881,15 +836,14 @@ describe('global AI tools', () => { }) const item = JSON.parse(raw) - expect(raw).not.toContain('draft-only content') expect(item).toMatchObject({ type: 'app', path: 'f/apps/report', - summary: 'saved app draft', + summary: 'deployed app', value: { frontend: [ - { path: '/src/App.tsx', size: 'draft content'.length }, - { path: '/src/DraftOnly.tsx', size: 'draft-only content'.length } + { path: '/src/App.tsx', size: 'deployed content'.length }, + { path: '/src/Helper.tsx', size: 'helper content'.length } ], backend: [ expect.objectContaining({ @@ -897,10 +851,10 @@ describe('global AI tools', () => { name: 'main', type: 'inline', language: 'bun', - contentSize: 'export async function main() { return "draft" }'.length + contentSize: 'export async function main() { return "deployed" }'.length }) ], - data: { tables: ['draft'] } + data: { tables: ['deployed'] } }, isDraft: false }) @@ -908,14 +862,14 @@ describe('global AI tools', () => { await expect( callGlobalTool('read_app_file', { path: 'f/apps/report', - file_path: '/src/DraftOnly.tsx' + file_path: '/src/Helper.tsx' }) - ).resolves.toBe('draft-only content') + ).resolves.toBe('helper content') expect(UserDraft.get('raw_app', 'f/apps/report', { workspace: WORKSPACE })).toBeUndefined() }) it('reads raw app files without creating a local draft', async () => { - vi.mocked(AppService.getAppByPathWithDraft).mockResolvedValueOnce({ + vi.mocked(AppService.getAppByPath).mockResolvedValueOnce({ path: 'f/apps/report', summary: 'deployed app', versions: [5], @@ -923,14 +877,6 @@ describe('global AI tools', () => { files: { '/src/App.tsx': 'deployed content' }, runnables: {}, data: { tables: [] } - }, - draft: { - summary: 'saved app draft', - value: { - files: { '/src/App.tsx': 'draft content' }, - runnables: {}, - data: { tables: [] } - } } } as any) @@ -939,12 +885,12 @@ describe('global AI tools', () => { path: 'f/apps/report', file_path: '/src/App.tsx' }) - ).resolves.toBe('draft content') + ).resolves.toBe('deployed content') expect(UserDraft.get('raw_app', 'f/apps/report', { workspace: WORKSPACE })).toBeUndefined() }) it('does not persist a raw app draft when patch_app_file validation fails', async () => { - vi.mocked(AppService.getAppByPathWithDraft).mockResolvedValueOnce({ + vi.mocked(AppService.getAppByPath).mockResolvedValueOnce({ path: 'f/apps/report', summary: 'deployed app', versions: [5], @@ -968,7 +914,7 @@ describe('global AI tools', () => { }) it('does not persist a raw app draft when delete_app_file validation fails', async () => { - vi.mocked(AppService.getAppByPathWithDraft).mockResolvedValueOnce({ + vi.mocked(AppService.getAppByPath).mockResolvedValueOnce({ path: 'f/apps/report', summary: 'deployed app', versions: [5], @@ -989,7 +935,7 @@ describe('global AI tools', () => { }) it('does not persist a raw app draft when delete_app_runnable validation fails', async () => { - vi.mocked(AppService.getAppByPathWithDraft).mockResolvedValueOnce({ + vi.mocked(AppService.getAppByPath).mockResolvedValueOnce({ path: 'f/apps/report', summary: 'deployed app', versions: [5], diff --git a/frontend/src/lib/components/copilot/chat/global/core.ts b/frontend/src/lib/components/copilot/chat/global/core.ts index 10d055938c..f093a8d91d 100644 --- a/frontend/src/lib/components/copilot/chat/global/core.ts +++ b/frontend/src/lib/components/copilot/chat/global/core.ts @@ -576,7 +576,12 @@ function serializeWorkspaceItemForRead(item: WorkspaceItem): unknown { } } - if (item.type === 'app' && item.value && typeof item.value === 'object' && 'files' in item.value) { + if ( + item.type === 'app' && + item.value && + typeof item.value === 'object' && + 'files' in item.value + ) { return { type: 'app', path: item.path, @@ -892,10 +897,9 @@ function appSourceToDraftValue(app: any, fallback?: any): AppDraftValue { } } -function appDraftMeta(app: { versions?: number[]; draft_created_at?: string }): UserDraftMeta { +function appDraftMeta(app: { versions?: number[] }): UserDraftMeta { return { - remoteRev: app.versions ? app.versions[app.versions.length - 1] : undefined, - remoteDraftRev: app.draft_created_at + remoteRev: app.versions ? app.versions[app.versions.length - 1] : undefined } } @@ -905,8 +909,8 @@ async function loadAppValueForRead(path: string, workspace: string): Promise { @@ -915,8 +919,8 @@ async function loadAppDraftValue(path: string, workspace: string): Promise void } -} -| { - mode: 'simple' - original: Value - current: Value - title: string - button?: { text: string; onClick: () => void } -} +export type DiffDrawerDiff = + | { + mode: 'normal' + deployed: Value + draft?: Value | undefined + current: Value + defaultDiffType?: 'deployed' | 'draft' + button?: { text: string; onClick: () => void } + } + | { + mode: 'simple' + original: Value + current: Value + title: string + button?: { text: string; onClick: () => void } + } export interface DiffDrawerI { - openDrawer: () => void - closeDrawer: () => void - setDiff: (diff: DiffDrawerDiff) => void -} \ No newline at end of file + openDrawer: () => void + closeDrawer: () => void + setDiff: (diff: DiffDrawerDiff) => void +} diff --git a/frontend/src/lib/components/flow_builder.ts b/frontend/src/lib/components/flow_builder.ts index e91a06e430..eb76d2e864 100644 --- a/frontend/src/lib/components/flow_builder.ts +++ b/frontend/src/lib/components/flow_builder.ts @@ -1,7 +1,7 @@ -import type { OpenFlow } from '$lib/gen' +import type { Flow, OpenFlow } from '$lib/gen' import type { StateStore } from '$lib/utils' import type { FlowState } from './flows/flowState' -import type { FlowWithDraftAndDraftTriggers, Trigger } from './triggers/utils' +import type { Trigger } from './triggers/utils' import type { DiffDrawerI } from './diff_drawer' import type { FlowBuilderWhitelabelCustomUi } from './custom_ui' import type { ScheduleTrigger } from './triggers' @@ -17,14 +17,13 @@ export type FlowBuilderProps = { loading?: boolean flowStore: StateStore flowStateStore: StateStore - savedFlow?: FlowWithDraftAndDraftTriggers | undefined + savedFlow?: Flow | undefined diffDrawer?: DiffDrawerI | undefined customUi?: FlowBuilderWhitelabelCustomUi disableAi?: boolean disabledFlowInputs?: boolean savedPrimarySchedule?: ScheduleTrigger | undefined // used to set the primary schedule in the legacy primaryScheduleStore version?: number | undefined - setSavedraftCb?: ((cb: () => void) => void) | undefined draftTriggersFromUrl?: Trigger[] | undefined selectedTriggerIndexFromUrl?: number | undefined children?: import('svelte').Snippet @@ -34,18 +33,6 @@ export type FlowBuilderProps = { } noInitial?: boolean liveEditorDraftStoragePath?: string - onSaveInitial?: ({ path, id }: { path: string; id: string }) => void - onSaveDraft?: ({ - path, - savedAtNewPath, - newFlow - }: { - path: string - savedAtNewPath: boolean - newFlow: boolean - }) => void - onSaveDraftError?: ({ error }: { error: any }) => void - onSaveDraftOnlyAtNewPath?: ({ path, selectedId }: { path: string; selectedId: string }) => void onDeploy?: ({ path }: { path: string }) => void onDeployError?: ({ error }: { error: any }) => void onDetails?: ({ path }: { path: string }) => void diff --git a/frontend/src/lib/components/flows/content/FlowEditorDrawer.svelte b/frontend/src/lib/components/flows/content/FlowEditorDrawer.svelte index 30311e2fa3..0ce0843bbf 100644 --- a/frontend/src/lib/components/flows/content/FlowEditorDrawer.svelte +++ b/frontend/src/lib/components/flows/content/FlowEditorDrawer.svelte @@ -24,23 +24,14 @@ flowEditorDrawer?.openDrawer?.() try { - const flowWithDraft = await FlowService.getFlowByPathWithDraft({ + const backendFlow = await FlowService.getFlowByPath({ workspace: $workspaceStore!, path }) - savedFlow = { - ...structuredClone(flowWithDraft), - draft: flowWithDraft.draft - ? { - ...structuredClone(flowWithDraft.draft), - path: flowWithDraft.draft.path ?? flowWithDraft.path - } - : undefined - } as Flow & { draft?: Flow } + savedFlow = structuredClone(backendFlow) as Flow - // Use the draft if available, otherwise the deployed flow - flow = flowWithDraft.draft ?? flowWithDraft + flow = backendFlow await initFlow(flow, flowStore, flowStateStore) loading = false @@ -53,11 +44,7 @@ let callback: (() => void) | undefined = undefined let flowPath: string = $state('') let flow: Flow | undefined = $state(undefined) - let savedFlow: - | (Flow & { - draft?: Flow | undefined - }) - | undefined = $state(undefined) + let savedFlow: Flow | undefined = $state(undefined) let loading = $state(true) const flowStore: StateStore = $state({ diff --git a/frontend/src/lib/components/home/ItemsList.svelte b/frontend/src/lib/components/home/ItemsList.svelte index d1950697f9..752ad060ae 100644 --- a/frontend/src/lib/components/home/ItemsList.svelte +++ b/frontend/src/lib/components/home/ItemsList.svelte @@ -61,7 +61,6 @@ type?: U time?: number starred?: boolean - has_draft?: boolean hash?: string } diff --git a/frontend/src/lib/components/home/treeViewUtils.ts b/frontend/src/lib/components/home/treeViewUtils.ts index 3196621a4d..1bbe013e46 100644 --- a/frontend/src/lib/components/home/treeViewUtils.ts +++ b/frontend/src/lib/components/home/treeViewUtils.ts @@ -5,7 +5,6 @@ type TableItem = T & { type?: U time?: number starred?: boolean - has_draft?: boolean } type TableScript = TableItem diff --git a/frontend/src/lib/components/raw_apps/RawAppEditorHeader.svelte b/frontend/src/lib/components/raw_apps/RawAppEditorHeader.svelte index 495a4ebcc1..9e6b961f93 100644 --- a/frontend/src/lib/components/raw_apps/RawAppEditorHeader.svelte +++ b/frontend/src/lib/components/raw_apps/RawAppEditorHeader.svelte @@ -5,7 +5,7 @@ import { editPathFor, invalidate as invalidatePicker } from '$lib/components/workspacePicker' import { invalidateWorkspacePaths } from '$lib/components/PathNameAutocomplete.svelte' - import { AppService, DraftService, type Policy } from '$lib/gen' + import { AppService, type Policy } from '$lib/gen' import { UserDraft } from '$lib/userDraft.svelte' import { rawAppToHubUrl } from '$lib/hub' import { enterpriseLicense, hubBaseUrlStore, userStore, workspaceStore } from '$lib/stores' @@ -26,12 +26,7 @@ WandSparkles } from 'lucide-svelte' import { createEventDispatcher, untrack } from 'svelte' - import { - cleanValueProperties, - orderedJsonStringify, - type Value, - replaceFalseWithUndefined - } from '../../utils' + import { orderedJsonStringify, type Value, replaceFalseWithUndefined } from '../../utils' import { random_adj } from '$lib/components/random_positive_adjetive' // import { allItems, toStatic } from '../apps/editor/settingsPanel/utils' @@ -51,7 +46,6 @@ import type { SavedAndModifiedValue } from '../common/confirmationModal/unsavedTypes' import DropdownV2 from '../DropdownV2.svelte' import { stateSnapshot } from '$lib/svelte5Utils.svelte' - import AppEditorHeaderDeployInitialDraft from '../apps/editor/AppEditorHeaderDeployInitialDraft.svelte' import AppEditorHeaderDeploy from '../apps/editor/AppEditorHeaderDeploy.svelte' import type { Runnable } from './RawAppInlineScriptRunnable.svelte' import { updateRawAppPolicy } from './rawAppPolicy' @@ -96,11 +90,9 @@ savedApp?: | { value: any - draft?: any path: string summary: string policy: any - draft_only?: boolean custom_path?: string } | undefined @@ -186,14 +178,12 @@ const loading = $state({ publish: false, - save: false, - saveDraft: false + save: false }) let pathError: string = $state('') let appExport = $state() as AppExportButton | undefined - let draftDrawerOpen = $state(false) let saveDrawerOpen = $state(false) let historyBrowserDrawerOpen = $state(false) let publishToHubDrawerOpen = $state(false) @@ -239,10 +229,6 @@ saveDrawerOpen = false } - function closeDraftDrawer() { - draftDrawerOpen = false - } - async function computeTriggerables() { policy = await updateRawAppPolicy(runnables, policy) } @@ -313,7 +299,7 @@ replaceFalseWithUndefined({ summary: summary, value: app, - path: newEditedPath || savedApp.draft?.path || savedApp.path, + path: newEditedPath || savedApp.path, policy, custom_path: customPath }) @@ -422,175 +408,6 @@ return } - async function saveInitialDraft() { - if (!app) { - sendUserToast(`App hasn't been loaded yet`, true) - return - } - await computeTriggerables() - try { - let { css, js } = await getBundle() - await AppService.createAppRaw({ - workspace: $workspaceStore!, - formData: { - app: { - value: app, - path: newEditedPath, - summary: summary, - policy, - draft_only: true, - custom_path: customPath - }, - js, - css - } - }) - await DraftService.createDraft({ - workspace: $workspaceStore!, - requestBody: { - path: newEditedPath, - typ: 'app', - value: { - value: app, - path: newEditedPath, - summary: summary, - policy, - custom_path: customPath - } - } - }) - savedApp = { - summary: summary, - value: structuredClone(stateSnapshot(app)), - path: newEditedPath, - policy, - draft_only: true, - draft: { - summary: summary, - value: structuredClone(stateSnapshot(app)), - path: newEditedPath, - policy, - custom_path: customPath - }, - custom_path: customPath - } - - draftDrawerOpen = false - // The initial draft was promoted to a real path on the backend — - // drop the autosave keyed on the prior (possibly empty) path so - // a future "+ App" click opens on a clean slate. - UserDraft.remove('raw_app', appPath) - dispatch('savedNewAppPath', newEditedPath) - } catch (e) { - sendUserToast(`Error saving initial draft: ${e.body ?? e.message}`, true) - } - draftDrawerOpen = false - } - - async function saveDraft(forceSave = false) { - if (!app) { - sendUserToast(`App hasn't been loaded yet`, true) - return - } - if (newApp) { - // initial draft - draftDrawerOpen = true - return - } - if (!savedApp) { - return - } - const draftOrDeployed = cleanValueProperties(savedApp.draft || savedApp) - const current = cleanValueProperties({ - summary: summary, - value: app, - path: newEditedPath || savedApp.draft?.path || savedApp.path, - policy - }) - if (!forceSave && orderedJsonStringify(draftOrDeployed) === orderedJsonStringify(current)) { - sendUserToast('No changes detected, ignoring', false, [ - { - label: 'Save anyway', - callback: () => { - saveDraft(true) - } - } - ]) - return - } - loading.saveDraft = true - try { - await computeTriggerables() - let path = appPath - if (savedApp.draft_only) { - await AppService.deleteApp({ - workspace: $workspaceStore!, - path: path - }) - let { css, js } = await getBundle() - - await AppService.createAppRaw({ - workspace: $workspaceStore!, - formData: { - app: { - value: app!, - summary: summary, - policy, - path: newEditedPath || path, - draft_only: true, - custom_path: customPath - }, - js, - css - } - }) - } - await DraftService.createDraft({ - workspace: $workspaceStore!, - requestBody: { - path: savedApp.draft_only ? newEditedPath || path : path, - typ: 'app', - value: { - value: app!, - summary: summary, - policy, - path: newEditedPath || path - } - } - }) - - savedApp = { - ...(savedApp?.draft_only - ? { - summary: summary, - value: structuredClone(stateSnapshot(app)), - path: savedApp.draft_only ? newEditedPath || path : path, - policy, - draft_only: true, - custom_path: customPath - } - : savedApp), - draft: { - summary: summary, - value: structuredClone(stateSnapshot(app)), - path: newEditedPath || path, - policy, - custom_path: customPath - } - } - - sendUserToast('Draft saved') - UserDraft.remove('raw_app', path) - loading.saveDraft = false - if (newApp || savedApp.draft_only) { - dispatch('savedNewAppPath', newEditedPath || path) - } - } catch (e) { - loading.saveDraft = false - throw e - } - } - let onLatest = $state(true) async function compareVersions() { if (version === undefined) { @@ -613,13 +430,6 @@ let moreItems = $derived([ ...(compactTopbar ? [ - { - displayName: 'Save draft', - icon: Save, - action: () => saveDraft(), - shortcut: `${mod}S`, - disabled: !newApp && !savedApp - }, { displayName: `Jobs (${jobs?.length > 99 ? '99+' : (jobs?.length ?? 0)})`, icon: Bug, @@ -685,11 +495,10 @@ diffDrawer?.setDiff({ mode: 'normal', deployed: deployedValue ?? savedApp, - draft: savedApp.draft, current: { summary: summary, value: app, - path: newEditedPath || savedApp.draft?.path || savedApp.path, + path: newEditedPath || savedApp.path, policy, custom_path: customPath } @@ -712,7 +521,7 @@ modifiedValue: { summary: summary, value: app, - path: newEditedPath || savedApp?.draft?.path || savedApp?.path, + path: newEditedPath || savedApp?.path, policy, custom_path: customPath } @@ -736,44 +545,19 @@ currentValue={{ summary: summary, value: app, - path: newEditedPath || savedApp?.draft?.path || savedApp?.path, + path: newEditedPath || savedApp?.path, policy, custom_path: customPath }} /> -{#if appPath == ''} - - closeDraftDrawer()}> - {#snippet actions()} -
- -
- {/snippet} - -
-
-{/if} closeSaveDrawer()}> {#snippet actions()}
{/if} - {#if !compactTopbar} - - {/if}