Repository navigation
Conversation
The controls, renderedWritten by Who else is in the workbook. Four badges and a count, beside the account. Share as a split button. The label opens the dialog. The chevron holds The share dialog, with access the provider reported. The link row carries The Open page. Two columns, the user's files and what other people shared. The overflow on a document. Open, Share and Work together. A folder has no menu. Only the |
The pages moved to craft-libs and the pixels did not change
Two pieces of evidence that nothing a user sees moved with it. The fifteen behaviour tests in The five renders are byte identical. I rendered them from the build before the Gates on the merged revisions: fmt, clippy under |
A cloud workbook now saves in the format its name asks for
The pages hand over the destination now, as a name and the content type the A provider that issued no extension still says what the thing is. The fixture DeckCraft found it. That application hit the same defect against One new test opens the extensionless CSV, edits it, saves, and asserts the |
|
Responsive title-bar fix pushed and deployed.
|
GridCraft now joins a CraftCollab room and both directions work. Verified from
a cross-compiled Windows binary installed on a workstation, against the live
relay on .88 with tokens minted by the Nextcloud app:
* a peer elsewhere set B2 to 7 and the installed app's own workbook holds 7.0
* the installed app set D4 to 42 and a listening peer logged
`APPLY cell.set {"cell":"D4","input":"42"}`, ordered at seq 25
Three contracts carry this. Nothing here is tied to one vendor.
`gridcraft_engine::CollabSink` is the only thing the engine gains. It hears
about a local command and knows nothing about what is listening. A relay, a log
and a test double are the same shape from there. Ordering, locking, transport
and credentials all stay outside the engine.
`craft_collab::Hub` is the transport, already defined in craft-libs.
`TokenSource` is how a credential arrives. `EnvToken` reads the environment.
That is the path `craft-collab`'s own `join` example already uses. A token
therefore works in both without translation.
`Session::execute` is the single chokepoint every command passes through. That
is where a local edit is forwarded. The predicate is `spec.journal`. It already
means "this changed the workbook". Commands that only move the view or
open a dialog are not journaled and are therefore not shared. That is correct
without a second list to maintain.
Inbound edits run through `Session::apply_remote`, identical to `execute`
except that it does not report to the sink. Without that an edit echoes between
two peers forever. It still journals and still records undo history. A
remote edit can be undone locally like any other.
The room is keyed on the Nextcloud file id and never on a path. A rename
mid-session therefore does not split the peers. `craft_collab::Session` discards
any message whose doc does not match its own. The token carries the file id.
The client derives a starting room from that, then lets the relay's own
`joined` replace it. Guessing it wrong looks exactly like a relay that answers
nothing.
Nothing is sent on connect. The relay admits a peer from the token in the
handshake and has no handler for a client-sent join. Sending one raced
socketioxide's namespace registration and cost a duplicate session every time.
`App` became a named-field struct. The tuple could not take another field
without the index shifting on macOS only, where two fields sit behind a cfg.
A frame asks for a repaint while a link is live. egui draws when something
happens locally. Another person typing is not a local event. Without that
repaint a remote edit sits in the socket until the mouse moves.
`craft-collab` is a path dependency on craft-libs, checked out beside this
repository. The wire protocol lives there because every Craft app has to speak
the same one.
Windows binaries cross-compile with cargo-xwin in a `rust:1-slim` container,
needing `clang lld llvm` and no MSVC toolchain on the target machine. Install
`llvm` as well as clang and lld: without it `build.rs` cannot find `llvm-rc`
and the binary ships with no icon and no VERSIONINFO, logging a warning and
carrying on.
GridCraft could co-edit a blank workbook and nothing else. It opened no real document and saved nowhere. Its Share button was an export affordance with no provider behind it. All three work now. No vendor is named anywhere. Every call goes through `craft-cloud-client` to CloudCraft. The app holds no provider credential, knows no provider URL and implements no provider protocol. `apps/gridcraft/src/nextcloud.rs` is gone. That is the point. It was an app speaking a provider's protocol and holding its credential on disk. File › Open grew a Cloud section with My files and Shared with me side by side. My files browses folders with a breadcrumb. Shared with me is flat, since an incoming share has no common parent folder. Each row names who shared it and whether it is read only. Filtering uses the extension and the MIME together. A WebDAV server often returns `application/octet-stream` for a perfectly good `.xlsx`. A web export often has the right type and no extension. Either test alone hides files the user can see in their own file manager. Save As grew a Where section: This PC, unchanged, or the cloud account by name. Save writes back with the entry's etag as `if_match`. **A conflict is never retried and never forced.** `Error::Conflict` means somebody else saved while this user was editing. A modal names the file and says nothing was written. It offers Reload, Save as a copy, or Keep editing. Getting that wrong loses work. It is the one failure here that matters. A real dialog, in `craft-ui` so every Craft app gets the same one. Multi-select recipients with autocomplete and removable chips, view or edit per share, an optional note, a notify box, the list of who already has access with a withdrawal beside each. Partial success is shown, never collapsed into one failure. Sharing with five people where one address is wrong shares with four and names the one that failed, leaving that recipient in the picker so a correction is one edit. **Nothing blocks the frame.** egui redraws at frame rate and these calls are synchronous. Each runs on a worker. The UI drains a channel. Reading a 20 MB workbook on the UI thread would freeze the window for seconds. **Autocomplete is debounced and ordered.** 200 ms after the last keystroke, with a sequence number per request so an answer the user has typed past is dropped before it reaches the list. **Capabilities gate the feature itself.** Shared with me is hidden when the provider does not offer it, the notify box is disabled without mail, and the share dialog will not open without sharing. Offering a control that fails on use is worse than not offering it.
The share dialog asks where and what to call it. Only this side can turn an open workbook into bytes, so it answers, reusing the same cloud::save_as that File > Save As already uses.
The environment variables and the session file follow the service, which is collabcraft now. CloudCraft was too close to several unrelated products to search for.
The account badge goes where every office suite puts it, at the far right of the title bar. craft-ui draws it. Every Craft app therefore gets the same one. Without it there was no way into the cloud at all. Every control is gated on an account being linked. A build with no badge therefore showed none of this work and looked exactly like the stock application. Drawing the badge starts nothing. The worker signs in before any job it is given. It then blocks for minutes polling the browser. Beginning that from a paint call opened a browser at launch and left every later job queued behind it. Three faults in the path this exposed, all of them older than the badge. `open_browser` handed the URL to `cmd` unquoted. `cmd` splits its command line on `&`. An authorize URL is mostly `&`. The browser therefore received the query up to the first one. The identity provider then reported that the client identifier was missing. By that point it was. The URL is quoted now. The quotes go through a raw command line, where they survive argument escaping. A sign-in that failed could not be retried. The latch that stops a frame asking twice was never cleared. The second press was swallowed. The share dialog asks what the provider can do until it gets an answer, then keeps it. On a cold start that answer is that nobody is signed in. Share stayed unavailable for the rest of the run however many times somebody signed in. It is told to ask again when the account changes. It is told again when storage finishes linking. The waiting panel gained Open in browser and Copy link. A bare label cannot be selected. When the browser does the wrong thing, the only way out is carrying the address across by hand. craft-libs is reached by a pinned revision now. That is how this repository already reaches craft-print and craft-ui. A sibling path only resolves where somebody has checked craft-libs out beside this one.
craft-libs was reviewed down to an interface and some controls. The provider, the client for the service and the relay client all moved to the repository that calls them. `craft-ui` lost its `collabcraft` feature with them. So the share backend comes from its own crate now. `craft-ui` draws the dialog and states each piece of work as a request. Nothing in craft-libs answers one. `collabcraft-share` is the half that opens a socket. It lives beside the client. Nothing about what this application does has changed. The contracts and the controls come from craft-libs as before. Only the three crates that reach a wire come from somewhere else.
Four controls existed in craft-ui. No application drew one. This draws them. The title bar shows who else is in the workbook, beside the account. Nothing appears while a person works alone. The relay sends a roster with everybody in it, the reader included. The row drops itself by asking the session which peer it is. A badge colour is derived from the peer identity. Its position in the list would do as well until somebody arrives or leaves. A cursor drawn by some later change has to reach the same colour for the same person. Share is a split button now. The label opens the share dialog, the common case. The chevron holds working together and the three Save a Copy formats that used to sit loose in the title bar. Each cloud document in the Open page carries an overflow menu. A click on the row still opens the document. Sharing and talking about it are rarer. Those sit behind the ellipsis. A folder has no menu. Opening one is the only thing to do with it. Work together asks the provider where to go, then opens what it names. Only a provider that reports the capability offers it at all. A workbook that has never been in the cloud has no handle to key a room on. That one is told to save there first. The session file now says which account it belongs to. A token identifies nothing on its own. The suite launcher keeps one keychain entry per account, keyed on the service and the subject the provider issued. Both are written beside the token once the service has answered. Three tests cover the new job. The conversation a provider names gets opened. A local workbook is told to save first. A provider with nowhere to talk never offers the item. A fourth test writes a render of each state to the directory in GRIDCRAFT_CLOUD_RENDERS, for a person to look at. The libraries moved under this. A share carries the name its provider gave the recipient. The access list reads as people now. The client grew a failure for a request it could not encode.
craft_ui::cloud now draws the Open page, the Save As destination, the account badge, the presence row, a document's overflow menu and the Share split button. crates/ui-egui/src/cloud.rs goes from 1328 lines to 243. What is left is what only GridCraft can answer. A document is a workbook. These nine extensions and these seven content types are what it opens. Bytes become one through file.open and go back through save_bytes. A refused save sends the user to GridCraft's own Save As page. The words are the ones i18n has. That is impl CloudApp for SheetApp, twenty-three methods of a line or two each. Services::cloud is gone. A backend is connected to the pages now, the way the share dialog's already was. The worker is unchanged apart from where it posts its answers. It holds the Post the job arrived with. Nothing outside it can reach the queue. SheetApp::account is gone too. The account badge is the pages' own control. SheetApp::presence stayed, since the collab roster sets it from main. All fifteen behaviour tests in crates/ui-egui/tests/cloud_files.rs pass with every assertion byte-identical. Two things in the harness changed. Both are the seam itself. The fake service implements CloudBackend now. It is connected with app.cloud.connect, where it used to arrive through Services. Nothing a user sees changed. The render test still writes the same five images.
The pin named a branch head. That branch is in main now.
A workbook opened from the cloud went back as XLSX whatever it arrived as. `encode` hardcoded `workbook.xlsx`. That was the only thing the pages told it. Open `figures.csv` from the cloud, edit a cell, save. The service then holds XLSX bytes under the name `figures.csv`. Download it and nothing opens it. Nothing ever said so. The pages hand over the destination now. The name decides, through the same `save_bytes` the desktop Save As uses. That refuses an extension this application cannot write and names the ones it can. A `.xls` from the cloud now says to Save As `.xlsx`. It used to become one quietly. A provider that issued no extension still says what the thing is. A file exported by a web application commonly has a correct content type and a bare name. The fixture in these tests is exactly that. The content type decides for the four formats a bare name can mean here. Everything else this application opens is a package format whose writer wants a real name. Letting `save_bytes` refuse is more use than a guess.
The browser build connects the shared cloud pages and share dialog to craft-cloud-client on the same origin, so session cookies reach the API. Desktop-only device-flow polling stays out of wasm; unauthenticated users go to the service login page.
Move AutoSave into the existing More menu below 560 px so cloud account, presence, and Share controls remain reachable without overlap.





Summary
Open, Save, Share, and shared editing against CollabCraft in GridCraft.
Depends on storytold/craft-libs#48 and #52. Pins currently on the fork until those land.
Related to storytold/craft-libs#123 (Add collab-craft repo and accept transfer).