diff --git a/src/app/mod.rs b/src/app/mod.rs index da3562b6..fafd02bf 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -3673,13 +3673,16 @@ impl OpenCADStudio { ); let thumbs_fetch = s.refresh_recent_thumbs(); - // CMMS embedding (not upstream): if launched from cad-edit.php, - // window.CAD_EDITOR_CONFIG is set and the host has already validated - // the attachment belongs to the job the editor was opened from - load - // it immediately instead of waiting for File > Open. Standalone use - // (opencadstudio.com, no config present) is unaffected; the Start tab - // just shows as normal. Mirrors on_open_file's wasm branch. - let cmms_load = if crate::sys::cmms_config().is_some() { + // CMMS embedding (not upstream): if launched from cad-edit.php to + // *edit* an existing attachment, window.CAD_EDITOR_CONFIG carries a + // load_url the host has already validated - load it immediately + // instead of waiting for File > Open, mirrors on_open_file's wasm + // branch. Gated on load_url specifically, not just cmms_config()'s + // presence: a "new file" launch (cad-edit.php?new=1) sets the config + // but omits load_url on purpose (nothing exists yet to load), and + // should boot to the normal blank Start tab, not show a load error. + // Standalone use (opencadstudio.com, no config at all) is unaffected. + let cmms_load = if crate::sys::cmms_config().is_some_and(|c| c.load_url.is_some()) { let state = std::sync::Arc::new(crate::io::OpenProgressState::new( crate::app::OPEN_PHASE_READING, )); diff --git a/src/app/update/file.rs b/src/app/update/file.rs index 3a4d1c4c..2f71266a 100644 --- a/src/app/update/file.rs +++ b/src/app/update/file.rs @@ -2217,8 +2217,9 @@ pub(super) fn on_open_file(&mut self) -> Task { Ok(bytes) => { if crate::sys::cmms_config().is_some() { let post_bytes = bytes.clone(); + let post_filename = filename.clone(); cmms_save_task = Task::perform( - async move { crate::sys::post_bytes_to_host(&post_bytes).await }, + async move { crate::sys::post_bytes_to_host(&post_bytes, &post_filename).await }, Message::CmmsSaveResult, ); } else { diff --git a/src/io/mod.rs b/src/io/mod.rs index e92e7f4a..4d4a6832 100644 --- a/src/io/mod.rs +++ b/src/io/mod.rs @@ -525,21 +525,24 @@ pub async fn pick_and_load_web( } /// CMMS embedding (not upstream): load the one file the host page already -/// validated, instead of showing the browser picker. `crate::sys::cmms_config()` -/// returning `None` means this build is running standalone (opencadstudio.com -/// or a plain local file), so callers should fall back to `pick_and_load_web` -/// in that case — this never shows a picker itself. +/// validated, instead of showing the browser picker. Returns `None` both +/// when this build is running standalone (no `cmms_config()` at all) and +/// when it's embedded but launched to create a brand-new file (`cad-edit.php +/// ?new=1` - config present, but no `load_url` since there's nothing to +/// load yet) - callers treat both the same: don't auto-load, let the app +/// boot to its normal blank Start tab. #[cfg(target_arch = "wasm32")] pub async fn load_from_cmms_web(progress: Arc) -> Option { use wasm_bindgen::JsCast; use wasm_bindgen_futures::JsFuture; let cfg = crate::sys::cmms_config()?; + let load_url = cfg.load_url?; progress.set(crate::app::OPEN_PHASE_READING, 500, 1, 2); - let name = cfg.original_name.clone(); + let name = cfg.original_name.clone().unwrap_or_else(|| "CMMS attachment".to_string()); let window = web_sys::window()?; - let response: web_sys::Response = JsFuture::from(window.fetch_with_str(&cfg.load_url)) + let response: web_sys::Response = JsFuture::from(window.fetch_with_str(&load_url)) .await .ok()? .dyn_into() diff --git a/src/sys.rs b/src/sys.rs index 0431ecc5..6210ba55 100644 --- a/src/sys.rs +++ b/src/sys.rs @@ -311,12 +311,15 @@ pub fn download_bytes(name: &str, bytes: &[u8]) { #[cfg(target_arch = "wasm32")] pub struct CmmsConfig { - pub load_url: String, pub save_url: String, - pub attachment_id: String, pub job_id: String, pub csrf_token: String, - pub original_name: String, + // Absent when the editor was launched to create a brand-new file + // (public/cad-edit.php?job_id=&new=1) rather than to edit an existing + // attachment - there is nothing to auto-load and nothing to update yet. + pub load_url: Option, + pub attachment_id: Option, + pub original_name: Option, } #[cfg(target_arch = "wasm32")] @@ -334,30 +337,46 @@ pub fn cmms_config() -> Option { .as_string() }; Some(CmmsConfig { - load_url: get_str("loadUrl")?, save_url: get_str("saveUrl")?, - attachment_id: get_str("attachmentId")?, job_id: get_str("jobId")?, csrf_token: get_str("csrfToken")?, - original_name: get_str("originalName")?, + load_url: get_str("loadUrl"), + attachment_id: get_str("attachmentId"), + original_name: get_str("originalName"), }) } /// POST the current document's bytes back to the CMMS in place of a browser /// download. Mirrors `download_bytes`'s Blob construction but sends it as a /// multipart file field instead of triggering an `` click. +/// +/// `cfg.attachment_id` decides create vs. update: present -> `cad_save` +/// (overwrite that exact attachment); absent -> `cad_save_new` (this is the +/// first save of a brand-new, editor-created file) using `filename` as the +/// new attachment's name. On a successful create, the new id is written back +/// into `window.CAD_EDITOR_CONFIG` so the *next* save on this same tab +/// updates that row instead of creating a duplicate each time - there is no +/// other state to thread through the app's own Message/Task plumbing for +/// this, the host page's own JS global is the simplest shared place to park it. #[cfg(target_arch = "wasm32")] -pub async fn post_bytes_to_host(bytes: &[u8]) -> Result<(), String> { - use wasm_bindgen::JsCast; +pub async fn post_bytes_to_host(bytes: &[u8], filename: &str) -> Result<(), String> { + use wasm_bindgen::{JsCast, JsValue}; use wasm_bindgen_futures::JsFuture; let cfg = cmms_config().ok_or("not running inside the CMMS")?; + let is_new = cfg.attachment_id.is_none(); let form = web_sys::FormData::new().map_err(|_| "could not build form data")?; - form.append_with_str("action", "cad_save") - .map_err(|_| "form append failed")?; - form.append_with_str("id", &cfg.attachment_id) + form.append_with_str("action", if is_new { "cad_save_new" } else { "cad_save" }) .map_err(|_| "form append failed")?; + if let Some(id) = &cfg.attachment_id { + form.append_with_str("id", id) + .map_err(|_| "form append failed")?; + } + if is_new { + form.append_with_str("original_name", filename) + .map_err(|_| "form append failed")?; + } form.append_with_str("job_id", &cfg.job_id) .map_err(|_| "form append failed")?; form.append_with_str("csrf_token", &cfg.csrf_token) @@ -385,6 +404,41 @@ pub async fn post_bytes_to_host(bytes: &[u8]) -> Result<(), String> { if !response.ok() { return Err(format!("save failed: HTTP {}", response.status())); } + + if is_new { + let text = JsFuture::from(response.text().map_err(|_| "text() unavailable")?) + .await + .map_err(|_| "save response body read failed")?; + let body = text.as_string().ok_or("save response body is not a string")?; + let parsed: serde_json::Value = + serde_json::from_str(&body).map_err(|_| "save response is not valid JSON")?; + let new_id = parsed + .get("id") + .and_then(|v| v.as_i64()) + .ok_or("save response missing id")?; + + let config_obj = js_sys::Reflect::get(&window, &JsValue::from_str("CAD_EDITOR_CONFIG")) + .map_err(|_| "could not re-read CAD_EDITOR_CONFIG")?; + js_sys::Reflect::set( + &config_obj, + &JsValue::from_str("attachmentId"), + &JsValue::from_str(&new_id.to_string()), + ) + .map_err(|_| "could not update attachmentId")?; + js_sys::Reflect::set( + &config_obj, + &JsValue::from_str("loadUrl"), + &JsValue::from_str(&format!("/attachment.php?id={new_id}")), + ) + .map_err(|_| "could not update loadUrl")?; + js_sys::Reflect::set( + &config_obj, + &JsValue::from_str("originalName"), + &JsValue::from_str(filename), + ) + .map_err(|_| "could not update originalName")?; + } + Ok(()) }