Repository navigation
Integrate CollabCraft cloud Open/Save/Share/presence #2765
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
wdunn001
wants to merge
40
commits into
storytold:main
Choose a base branch
from
wdunn001:cloud-pages
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+2,322
−342
Open
Changes from all commits
Commits
Show all changes
40 commits
Select commit
Hold shift + click to select a range
caaea1b
Integrate CollabCraft cloud Open/Save/Share/presence into PdfCraft
wdunn001 211d2cf
Fix atomic session-file write and service-origin token isolation
wdunn001 683e091
Fix egui 0.36 API breaks in cloud icon and style
wdunn001 5455c23
Clean up warnings found by Linux build: unused import and dead code
wdunn001 5121140
Bootstrap cloud account on startup so root login carries into PDFCraft
wdunn001 9e45be1
Fix callback and logout routing for browser cloud session
wdunn001 f9471c5
Fix: access cloud.status via field, not trait method
wdunn001 f06af60
Fix: Window::closed() returns Result<bool, JsValue>
wdunn001 0e203cd
Render cloud open/save pages in CentralPanel; add File menu and tab-s…
wdunn001 be9419f
Fix cloud_tests: use Harness::new_eframe, rewrite conflict test with …
wdunn001 6d758ec
Fix shapes_from_page: eliminate unsafe by moving Option<&mut App> int…
wdunn001 69adcbd
Fix cloud_page_shell Back button: use text button so label appears as…
wdunn001 3a3dcd0
Fix unlinked account regression expectation
wdunn001 fb35c2c
Fix account test: NotLinked maps to link=Offered not account_fault; a…
wdunn001 f764bdb
Remove unused Link import from cloud_tests
wdunn001 68ee7c8
i18n: add CollabCraft cloud open/save strings to all locales
wdunn001 1ee9fe9
test(commands): skip cloud ids without CollabCraft service
wdunn001 717e227
fix(pickers): consume pick_override once per dialog chain
wdunn001 9fc81cc
test(commands): use run_inline for picker-driven open flows
wdunn001 f090c2d
Name CollabCraft shares with the pdfcraft app
wdunn001 ea0fc63
Merge for_app invite naming into cloud-pages
wdunn001 427d195
Merge storytold/pdfcraft main into cloud-pages; keep main i18n
wdunn001 b8a6574
Fix merge conflict fallout in manifests and docs
wdunn001 25d0366
Regenerate Cargo.lock after merge fix
wdunn001 2b7bac1
Wire Request signatures through CollabCraft e-sign.
wdunn001 d0f2363
Box EnvelopeDraft in Request::Create for craft-ui clippy.
wdunn001 0e7f0c4
Pin craft-libs to clippy-clean esign-controls tip.
wdunn001 a4edbaf
rustfmt esign UI host wiring for xtask ci.
wdunn001 c7a9252
Fix e-sign sender Identity from cloud Account fields.
wdunn001 9a72ff7
clippy: matches! and drop redundant cloud_page_shell closures.
wdunn001 9cfb6be
Unify collab-craft pin with craft-libs esign tip.
wdunn001 fda0d41
clippy: move open_browser before tests; static draw FnOnce.
wdunn001 dfa9709
Remove duplicate open_browser after reorder.
wdunn001 52e225e
Open a keyed CollabCraft invite in the web app instead of a blank doc…
wdunn001 8566d2a
Merge Request signatures e-sign UI into cloud-pages.
wdunn001 41f439e
Merge storytold/pdfcraft main into cloud-pages.
wdunn001 00ceb22
Address CodeRabbit review on cloud Open/Save integration.
wdunn001 6cd7c19
Address CodeRabbit login popup and e-sign form commit findings.
wdunn001 aaa6fe9
Open wasm login popup from account-menu sign-in gesture.
wdunn001 acff857
Pin craft-ui lockfile to sign-in gesture rev.
wdunn001 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,151 @@ | ||
| //! CollabCraft transport for the browser build. | ||
| //! | ||
| //! The same jobs and events as the desktop worker ([`pdfcraft::cloud`]), but | ||
| //! the client is async and the session rides the same-origin cookie the service | ||
| //! set at sign-in. There is no device flow and no token file. | ||
| //! | ||
| //! ## Login and logout routing | ||
| //! | ||
| //! When the account check returns Unauthenticated the transport reports the | ||
| //! fault and the title bar shows Sign in. The login popup opens only when the | ||
| //! user clicks Sign in (see [`pdfcraft_ui_egui::web_auth`] and [`pdfcraft_ui_egui::cloud::pump`]). | ||
| //! | ||
| //! A polling closure watches the popup and, when it closes, stores the key | ||
| //! `"pdfcraft:auth_done"` in egui's ephemeral memory and requests a repaint. | ||
| //! The update loop sees the flag, clears it, and calls `sign_in()` to force a | ||
| //! fresh `Job::Account`. | ||
| //! | ||
| //! Sign-out navigates to the service root `/` only after `/auth/logout` | ||
| //! succeeds. Reloading the PDFCraft page on a failed logout would leave the | ||
| //! session cookie active while the user believes they signed out. | ||
|
|
||
| use craft_cloud_client::{Client, Error}; | ||
| use pdfcraft_ui_egui::cloud::{Account, CloudBackend, Event, Fault, Job, Post}; | ||
| use wasm_bindgen::JsCast as _; | ||
| use wasm_bindgen_futures::spawn_local; | ||
|
|
||
| /// Hands cloud jobs to async tasks. Cheap to clone. | ||
| #[derive(Clone)] | ||
| pub struct Service { | ||
| client: Client, | ||
| ctx: egui::Context, | ||
| } | ||
|
|
||
| impl Service { | ||
| /// Always available when the app is served by CollabCraft on the same | ||
| /// origin. The page names the service; nothing is compiled in. | ||
| pub fn new(ctx: egui::Context) -> Service { | ||
| log::info!("cloud storage through {}", Client::from_page().base()); | ||
| Service { client: Client::from_page(), ctx } | ||
| } | ||
| } | ||
|
|
||
| impl CloudBackend for Service { | ||
| fn submit(&self, job: Job, post: Post) { | ||
| let worker = self.clone(); | ||
| spawn_local(async move { | ||
| worker.run(job, post).await; | ||
| }); | ||
| } | ||
| } | ||
|
|
||
| impl Service { | ||
| async fn run(self, job: Job, post: Post) { | ||
| let post = |event: Event| { | ||
| post.send(event); | ||
| self.ctx.request_repaint(); | ||
| }; | ||
| match job { | ||
| Job::Account => { | ||
| let answer = | ||
| self.me().await.map(|me| Account { display: me.display, capabilities: me.capabilities, provider_linked: me.provider_linked }); | ||
| post(Event::Account(answer)); | ||
| } | ||
| Job::LinkStart => match self.client.link_start().await.map_err(fault) { | ||
| Ok(start) => post(Event::Browser { url: start.login_url, handle: Some(start.handle) }), | ||
| Err(f) => post(Event::Linked(Err(f))), | ||
| }, | ||
| Job::LinkPoll(handle) => { | ||
| let answer = self.client.link_poll(&handle).await.map_err(fault); | ||
| post(Event::Linked(answer)); | ||
| } | ||
| Job::SignOut => { | ||
| if let Err(e) = fetch_logout().await { | ||
| log::warn!("sign-out failed: {e}"); | ||
| post(Event::Account(Err(Fault::Refused(format!("Sign-out failed: {e}"))))); | ||
| return; | ||
| } | ||
| if let Some(window) = web_sys::window() { | ||
| let _ = window.location().assign("/"); | ||
| } | ||
| } | ||
| Job::List(path) => { | ||
| let entries = self.client.list(&path).await.map_err(fault); | ||
| post(Event::Listed { path, entries }); | ||
| } | ||
| Job::SharedWithMe => { | ||
| let answer = self.client.shared_with_me().await.map_err(fault); | ||
| post(Event::Shared(answer)); | ||
| } | ||
| Job::Read { entry, into } => { | ||
| let id = entry.id.clone(); | ||
| let bytes = self.client.read(&id).await.map_err(fault); | ||
| post(Event::Read { entry, into, bytes }); | ||
| } | ||
| Job::Write { uid, entry, bytes } => { | ||
| let id = entry.id.clone(); | ||
| let etag = entry.etag.clone(); | ||
| let answer = self.client.write(&id, &bytes, etag.as_deref()).await.map_err(fault); | ||
| post(Event::Saved { uid, entry: answer }); | ||
| } | ||
| Job::Create { uid, folder, name, bytes } => { | ||
| let answer = self.client.create(&folder, &name, &bytes).await.map_err(fault); | ||
| post(Event::Saved { uid, entry: answer }); | ||
| } | ||
| Job::Conversation(id) => { | ||
| let answer = self.client.conversation(&id).await.map_err(fault); | ||
| post(Event::Conversation(answer)); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| async fn me(&self) -> Result<craft_cloud_client::Me, Fault> { | ||
| self.client.me().await.map_err(fault) | ||
| } | ||
| } | ||
|
|
||
| fn fault(e: Error) -> Fault { | ||
| match e { | ||
| Error::Unauthenticated => Fault::Unauthenticated, | ||
| Error::NotLinked => Fault::NotLinked, | ||
| Error::Unreachable(detail) => Fault::Unreachable(detail), | ||
| Error::Refused { code, .. } if code == "conflict" || code == "409" || code == "412" => Fault::Conflict, | ||
| Error::Refused { code, detail } => Fault::Refused(if detail.trim().is_empty() { code } else { detail }), | ||
| Error::Malformed(detail) => Fault::Refused(format!("the cloud service gave an answer PdfCraft could not read: {detail}")), | ||
| Error::Unsendable(detail) => { | ||
| log::error!("cloud request could not be encoded: {detail}"); | ||
| Fault::Refused("PdfCraft could not build that request".to_owned()) | ||
| } | ||
| } | ||
| } | ||
|
|
||
| async fn fetch_logout() -> Result<(), String> { | ||
| let global = js_sys::global(); | ||
| let fetch = js_sys::Reflect::get(&global, &wasm_bindgen::JsValue::from_str("fetch")) | ||
| .ok() | ||
| .and_then(|v| v.dyn_into::<js_sys::Function>().ok()) | ||
| .ok_or_else(|| "no fetch".to_owned())?; | ||
| let init = web_sys::RequestInit::new(); | ||
| init.set_method("POST"); | ||
| init.set_credentials(web_sys::RequestCredentials::Include); | ||
| let req = web_sys::Request::new_with_str_and_init("/auth/logout", &init).map_err(|e| format!("{e:?}"))?; | ||
| let promised = fetch.call1(global.as_ref(), req.as_ref()).map_err(|e| format!("{e:?}"))?; | ||
| let promised: js_sys::Promise = promised.dyn_into().map_err(|_| "not a promise".to_owned())?; | ||
| let resp = wasm_bindgen_futures::JsFuture::from(promised).await.map_err(|e| format!("{e:?}"))?; | ||
| let resp: web_sys::Response = resp.dyn_into().map_err(|_| "not a response".to_owned())?; | ||
| if resp.ok() { | ||
| Ok(()) | ||
| } else { | ||
| Err(format!("HTTP {}", resp.status())) | ||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.