//! The Askama entry point for a described Export CSV button. //! //! The first act taken through the pattern `27d5e5b8` ruled on: a described //! fragment inside an Askama page, at the granularity of one primitive, in a //! glue module that is a sibling of `widgets/` rather than a member of it. The //! whole of the reasoning is in [`super::widgets`]; what is here is the first //! thing it buys. //! //! # What it replaces //! //! `data-action="exportCsvButton" data-arg="/api/export/sales" //! data-arg2="sales.csv"` — a class naming a behaviour plus the two things the //! behaviour needed, positionally, repeated at five sites in four templates, //! backed by a `window.exportCsvButton` global that fetched the URL, built an //! anchor, set `download`, clicked it, and put the button's own label back //! afterwards. //! //! [`Action::saving`] says all of it in one sentence: the answer to this call //! is a file the reader keeps rather than a view. `htmx-glue.ts` performs it //! once for everything that says so, which it already did for //! [`super::buyer_contacts`], the one screen described before this. A terminal //! renderer writes the file to disk from the same sentence without being told //! which button this is. //! //! # One act, five call sites, and why the URL is a parameter //! //! The five exports differ in exactly two things: where the rows come from and //! what the file is called. Both are facts about the caller rather than about //! the control, so they are arguments and there is one function. Compare //! `upload_field`, where the accept list is a fact about MNW and belongs to the //! module: the test is whether a template would be writing the same literal //! twice if it were not here. //! //! # The visual change, which is the ruling's accepted cost //! //! The templates wrote `class="btn-secondary text-sm"`. A described act emits //! `class="button"`, so these five land on the design system's button. The //! layout-only classes a caller needs (`broadcast-export`, `nowrap`) stay in //! Askama on the element around it, where they were always a fact about that //! page's layout rather than about the control. use quasi_router::{Action, Node}; /// The route each export posts to, and the name the file is kept under. /// /// A read would be a link the browser downloads by itself; these are posts, /// because assembling the file is work the server does rather than a document /// it already has, and `Action::saves` is independent of the verb for exactly /// that reason. /// /// `awaiting` because the whole file is assembled before any of it comes back. /// Nothing countable to say about the wait: the row count is often known at the /// call site and the byte count never is, and `layout::Awaiting` takes a /// measurement rather than a stand-in for one. #[must_use] pub fn act(route: &str, filename: &str) -> Node { Node::act( "Export CSV", Action::post(route).saving(filename).awaiting(), ) } /// The same act as a fragment, for a template to drop in place. /// /// No shell and no region: this lands inside a document Askama has already /// built, which is what every glue module here does. #[must_use] pub fn html(route: &str, filename: &str) -> String { use quasi_axum::Serves as _; quasi_webview::Webview::new().fragment(&act(route, filename)) } #[cfg(test)] mod tests { /// The three things `data-action` + two positional arguments used to say, /// said once. `data-saves` is the whole of the host's instruction. #[test] fn an_export_says_where_it_reads_from_and_what_the_file_is_called() { let html = super::html("/api/export/sales", "sales.csv"); assert!(html.contains(r#"hx-post="/api/export/sales""#), "{html}"); assert!(html.contains(r#"data-saves="sales.csv""#), "{html}"); assert!(html.contains("Export CSV"), "{html}"); } /// The mark that says the call waits. A file the server assembles is the /// report case `Action::awaiting` exists for, and the button locking itself /// is what the deleted global did by hand. #[test] fn an_export_says_it_waits() { let html = super::html("/api/export/sales", "sales.csv"); assert!(html.contains("data-awaiting="), "{html}"); } /// Nothing positional survives. The failure this replaced was a second /// argument drifting from the first at one of five sites, silently. #[test] fn no_call_site_carries_a_behaviour_by_name() { let html = super::html("/api/export/followers", "followers.csv"); assert!(!html.contains("data-action"), "{html}"); assert!(!html.contains("data-arg"), "{html}"); } }