CMMS embedding: load-from-URL on boot, POST-save instead of download
Additive only - standalone/opencadstudio.com use is unaffected since every new path is gated on window.CAD_EDITOR_CONFIG being present (set by the CMMS's cad-edit.php host page, absent everywhere else). - src/sys.rs: cmms_config() reads window.CAD_EDITOR_CONFIG; post_bytes_to_host() POSTs saved bytes back via FormData instead of triggering a download. - src/io/mod.rs: load_from_cmms_web() fetches bytes from the host-provided loadUrl and feeds them through the existing load_web_bytes parse path. - src/app/mod.rs: boot_web() auto-loads via load_from_cmms_web() when embedded; CmmsSaveResult message variant carries the save outcome. - src/app/update/mod.rs: CmmsSaveResult handler surfaces success/failure in the command line. - src/app/update/file.rs: web save branch calls post_bytes_to_host() instead of download_bytes() when embedded. - Cargo.toml: add Request/RequestInit/FormData to the web-sys feature list.
This commit is contained in:
parent
52432af928
commit
494ddfece9
6 changed files with 221 additions and 5 deletions
|
|
@ -98,6 +98,9 @@ web-sys = { version = "=0.3.85", features = [
|
|||
"WritableStream",
|
||||
"Url",
|
||||
"Response",
|
||||
"Request",
|
||||
"RequestInit",
|
||||
"FormData",
|
||||
"Worker",
|
||||
"WorkerOptions",
|
||||
"WorkerType",
|
||||
|
|
|
|||
|
|
@ -1792,6 +1792,10 @@ pub enum Message {
|
|||
/// Web: an asynchronous OPFS copy written after Save is ready for recents.
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
WebRecentStored(Result<PathBuf, String>),
|
||||
/// CMMS embedding (not upstream): result of POSTing the saved bytes back
|
||||
/// to the host page instead of triggering a browser download.
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
CmmsSaveResult(Result<(), String>),
|
||||
SaveFile,
|
||||
SaveAs,
|
||||
// ── Custom Save-As dialog ─────────────────────────────────────────────
|
||||
|
|
@ -3668,9 +3672,53 @@ impl OpenCADStudio {
|
|||
Message::DiscussionsFetched,
|
||||
);
|
||||
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() {
|
||||
let state = std::sync::Arc::new(crate::io::OpenProgressState::new(
|
||||
crate::app::OPEN_PHASE_READING,
|
||||
));
|
||||
let open_id = s.next_open_id();
|
||||
s.opening = Some(crate::app::OpenProgress {
|
||||
id: open_id,
|
||||
name: "Opening…".into(),
|
||||
source_path: None,
|
||||
size_bytes: 0,
|
||||
state: state.clone(),
|
||||
started: Instant::now(),
|
||||
recovery_error: None,
|
||||
recovery_read_stats: None,
|
||||
recovery_bytes: None,
|
||||
});
|
||||
Task::perform(crate::io::load_from_cmms_web(state), move |outcome| {
|
||||
// cmms_config() was Some a moment ago (checked just above);
|
||||
// load_from_cmms_web only returns None if that vanishes
|
||||
// between here and its own first line, which can't happen in
|
||||
// a single-threaded wasm event loop - the fallback below
|
||||
// exists so a future refactor that breaks that invariant
|
||||
// fails as a visible in-app error, not a panic.
|
||||
let outcome = outcome.unwrap_or_else(|| crate::io::WebOpenOutcome {
|
||||
name: "CMMS attachment".to_string(),
|
||||
size_bytes: 0,
|
||||
result: Err(crate::io::OpenLoadError::from("CMMS integration unavailable")),
|
||||
recovery_bytes: None,
|
||||
cache_bytes: None,
|
||||
record_recent: false,
|
||||
});
|
||||
Message::WebFileOpened(open_id, outcome)
|
||||
})
|
||||
} else {
|
||||
Task::none()
|
||||
};
|
||||
|
||||
(
|
||||
s,
|
||||
Task::batch([focus, fonts, patrons, videos, discussions, thumbs_fetch]),
|
||||
Task::batch([focus, fonts, patrons, videos, discussions, thumbs_fetch, cmms_load]),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2204,13 +2204,26 @@ pub(super) fn on_open_file(&mut self) -> Task<Message> {
|
|||
self.sync_solid_models_to_acis(i);
|
||||
self.stamp_thumbnail(i, version);
|
||||
let mut recent_task = Task::none();
|
||||
// CMMS embedding (not upstream): when launched from
|
||||
// cad-edit.php, save POSTs back to the CMMS instead of
|
||||
// triggering a browser download. Standalone use (no
|
||||
// window.CAD_EDITOR_CONFIG) is unaffected.
|
||||
let mut cmms_save_task = Task::none();
|
||||
let saved = match crate::io::save_to_bytes(
|
||||
&self.tabs[i].scene.document,
|
||||
ext,
|
||||
version,
|
||||
) {
|
||||
Ok(bytes) => {
|
||||
crate::sys::download_bytes(&filename, &bytes);
|
||||
if crate::sys::cmms_config().is_some() {
|
||||
let post_bytes = bytes.clone();
|
||||
cmms_save_task = Task::perform(
|
||||
async move { crate::sys::post_bytes_to_host(&post_bytes).await },
|
||||
Message::CmmsSaveResult,
|
||||
);
|
||||
} else {
|
||||
crate::sys::download_bytes(&filename, &bytes);
|
||||
}
|
||||
let cache_name = std::path::Path::new(&filename)
|
||||
.file_name()
|
||||
.map(|name| name.to_string_lossy().into_owned())
|
||||
|
|
@ -2247,14 +2260,14 @@ pub(super) fn on_open_file(&mut self) -> Task<Message> {
|
|||
{
|
||||
let cont = self.update(Message::TabClose(idx));
|
||||
let rest = self.continue_tab_close_queue();
|
||||
return Task::batch([close, recent_task, cont, rest]);
|
||||
return Task::batch([close, recent_task, cmms_save_task, cont, rest]);
|
||||
}
|
||||
} else if self.pending_close.is_some() {
|
||||
let retry = self.open_unsaved_dialog_window();
|
||||
return Task::batch([close, recent_task, retry]);
|
||||
return Task::batch([close, recent_task, cmms_save_task, retry]);
|
||||
}
|
||||
}
|
||||
Task::batch([close, recent_task])
|
||||
Task::batch([close, recent_task, cmms_save_task])
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -828,6 +828,17 @@ impl OpenCADStudio {
|
|||
}
|
||||
},
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
Message::CmmsSaveResult(result) => {
|
||||
match result {
|
||||
Ok(()) => self.command_line.push_output("Saved to CMMS."),
|
||||
Err(error) => self
|
||||
.command_line
|
||||
.push_error(crate::tf!("Save to CMMS failed: {error}").as_ref()),
|
||||
}
|
||||
Task::none()
|
||||
}
|
||||
|
||||
Message::ImagePick => {
|
||||
Task::perform(crate::io::pick_image_file(), Message::ImagePickResult)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -524,6 +524,59 @@ 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.
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
pub async fn load_from_cmms_web(progress: Arc<OpenProgressState>) -> Option<WebOpenOutcome> {
|
||||
use wasm_bindgen::JsCast;
|
||||
use wasm_bindgen_futures::JsFuture;
|
||||
|
||||
let cfg = crate::sys::cmms_config()?;
|
||||
progress.set(crate::app::OPEN_PHASE_READING, 500, 1, 2);
|
||||
let name = cfg.original_name.clone();
|
||||
|
||||
let window = web_sys::window()?;
|
||||
let response: web_sys::Response = JsFuture::from(window.fetch_with_str(&cfg.load_url))
|
||||
.await
|
||||
.ok()?
|
||||
.dyn_into()
|
||||
.ok()?;
|
||||
if !response.ok() {
|
||||
return Some(WebOpenOutcome {
|
||||
name,
|
||||
size_bytes: 0,
|
||||
result: Err(OpenLoadError::from(format!(
|
||||
"CMMS load failed: HTTP {}",
|
||||
response.status()
|
||||
))),
|
||||
recovery_bytes: None,
|
||||
cache_bytes: None,
|
||||
record_recent: false,
|
||||
});
|
||||
}
|
||||
let buffer = JsFuture::from(response.array_buffer().ok()?).await.ok()?;
|
||||
let bytes: Arc<[u8]> = Arc::from(js_sys::Uint8Array::new(&buffer).to_vec());
|
||||
let size_bytes = bytes.len() as u64;
|
||||
|
||||
let result = load_web_bytes(&name, &bytes, progress.clone(), false, "", None).await;
|
||||
let keep_for_recovery = result
|
||||
.as_ref()
|
||||
.err()
|
||||
.is_some_and(|error| error.recovery_available);
|
||||
let cache_bytes = result.is_ok().then(|| Arc::clone(&bytes));
|
||||
Some(WebOpenOutcome {
|
||||
name,
|
||||
size_bytes,
|
||||
result,
|
||||
recovery_bytes: keep_for_recovery.then(|| Arc::clone(&bytes)),
|
||||
cache_bytes,
|
||||
record_recent: false,
|
||||
})
|
||||
}
|
||||
|
||||
/// Reopen a browser-private recent copy without showing the file picker.
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
pub async fn open_recent_web(
|
||||
|
|
|
|||
88
src/sys.rs
88
src/sys.rs
|
|
@ -300,6 +300,94 @@ pub fn download_bytes(name: &str, bytes: &[u8]) {
|
|||
let _ = web_sys::Url::revoke_object_url(&url);
|
||||
}
|
||||
|
||||
// --- CMMS embedding (not upstream) -----------------------------------------
|
||||
//
|
||||
// When this build is launched from the CMMS's cad-edit.php (rather than
|
||||
// standalone at opencadstudio.com), the host page sets `window.CAD_EDITOR_CONFIG`
|
||||
// before loading this bundle. Its presence is the sole signal that we're
|
||||
// embedded: absent, every function below returns None/Err and the normal
|
||||
// file-picker/download flow (pick_and_load_web / download_bytes) is
|
||||
// unaffected, so the same build works standalone or embedded.
|
||||
|
||||
#[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,
|
||||
}
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
pub fn cmms_config() -> Option<CmmsConfig> {
|
||||
use wasm_bindgen::JsValue;
|
||||
|
||||
let window = web_sys::window()?;
|
||||
let config = js_sys::Reflect::get(&window, &JsValue::from_str("CAD_EDITOR_CONFIG")).ok()?;
|
||||
if config.is_undefined() || config.is_null() {
|
||||
return None;
|
||||
}
|
||||
let get_str = |key: &str| -> Option<String> {
|
||||
js_sys::Reflect::get(&config, &JsValue::from_str(key))
|
||||
.ok()?
|
||||
.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")?,
|
||||
})
|
||||
}
|
||||
|
||||
/// 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 `<a download>` click.
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
pub async fn post_bytes_to_host(bytes: &[u8]) -> Result<(), String> {
|
||||
use wasm_bindgen::JsCast;
|
||||
use wasm_bindgen_futures::JsFuture;
|
||||
|
||||
let cfg = cmms_config().ok_or("not running inside the CMMS")?;
|
||||
|
||||
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)
|
||||
.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)
|
||||
.map_err(|_| "form append failed")?;
|
||||
|
||||
let array = js_sys::Uint8Array::from(bytes);
|
||||
let parts = js_sys::Array::new();
|
||||
parts.push(&array.buffer());
|
||||
let blob = web_sys::Blob::new_with_u8_array_sequence(&parts)
|
||||
.map_err(|_| "could not build blob")?;
|
||||
form.append_with_blob("file", &blob)
|
||||
.map_err(|_| "form append failed")?;
|
||||
|
||||
let window = web_sys::window().ok_or("no window")?;
|
||||
let opts = web_sys::RequestInit::new();
|
||||
opts.set_method("POST");
|
||||
opts.set_body(&form);
|
||||
let request = web_sys::Request::new_with_str_and_init(&cfg.save_url, &opts)
|
||||
.map_err(|_| "could not build request")?;
|
||||
let response: web_sys::Response = JsFuture::from(window.fetch_with_request(&request))
|
||||
.await
|
||||
.map_err(|_| "save request failed")?
|
||||
.dyn_into()
|
||||
.map_err(|_| "save response is invalid")?;
|
||||
if !response.ok() {
|
||||
return Err(format!("save failed: HTTP {}", response.status()));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Short platform string for bug reports: OS + architecture on the desktop,
|
||||
/// the browser user-agent on the web.
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
|
|
|
|||
Loading…
Reference in a new issue