Skip to main content

max / makenotwork

4.6 KB · 107 lines History Blame Raw
1 //! The Askama entry point for a described Export CSV button.
2 //!
3 //! The first act taken through the pattern `27d5e5b8` ruled on: a described
4 //! fragment inside an Askama page, at the granularity of one primitive, in a
5 //! glue module that is a sibling of `widgets/` rather than a member of it. The
6 //! whole of the reasoning is in [`super::widgets`]; what is here is the first
7 //! thing it buys.
8 //!
9 //! # What it replaces
10 //!
11 //! `data-action="exportCsvButton" data-arg="/api/export/sales"
12 //! data-arg2="sales.csv"` — a class naming a behaviour plus the two things the
13 //! behaviour needed, positionally, repeated at five sites in four templates,
14 //! backed by a `window.exportCsvButton` global that fetched the URL, built an
15 //! anchor, set `download`, clicked it, and put the button's own label back
16 //! afterwards.
17 //!
18 //! [`Action::saving`] says all of it in one sentence: the answer to this call
19 //! is a file the reader keeps rather than a view. `htmx-glue.ts` performs it
20 //! once for everything that says so, which it already did for
21 //! [`super::buyer_contacts`], the one screen described before this. A terminal
22 //! renderer writes the file to disk from the same sentence without being told
23 //! which button this is.
24 //!
25 //! # One act, five call sites, and why the URL is a parameter
26 //!
27 //! The five exports differ in exactly two things: where the rows come from and
28 //! what the file is called. Both are facts about the caller rather than about
29 //! the control, so they are arguments and there is one function. Compare
30 //! `upload_field`, where the accept list is a fact about MNW and belongs to the
31 //! module: the test is whether a template would be writing the same literal
32 //! twice if it were not here.
33 //!
34 //! # The visual change, which is the ruling's accepted cost
35 //!
36 //! The templates wrote `class="btn-secondary text-sm"`. A described act emits
37 //! `class="button"`, so these five land on the design system's button. The
38 //! layout-only classes a caller needs (`broadcast-export`, `nowrap`) stay in
39 //! Askama on the element around it, where they were always a fact about that
40 //! page's layout rather than about the control.
41
42 use quasi_router::{Action, Node};
43
44 /// The route each export posts to, and the name the file is kept under.
45 ///
46 /// A read would be a link the browser downloads by itself; these are posts,
47 /// because assembling the file is work the server does rather than a document
48 /// it already has, and `Action::saves` is independent of the verb for exactly
49 /// that reason.
50 ///
51 /// `awaiting` because the whole file is assembled before any of it comes back.
52 /// Nothing countable to say about the wait: the row count is often known at the
53 /// call site and the byte count never is, and `layout::Awaiting` takes a
54 /// measurement rather than a stand-in for one.
55 #[must_use]
56 pub fn act(route: &str, filename: &str) -> Node {
57 Node::act(
58 "Export CSV",
59 Action::post(route).saving(filename).awaiting(),
60 )
61 }
62
63 /// The same act as a fragment, for a template to drop in place.
64 ///
65 /// No shell and no region: this lands inside a document Askama has already
66 /// built, which is what every glue module here does.
67 #[must_use]
68 pub fn html(route: &str, filename: &str) -> String {
69 use quasi_axum::Serves as _;
70
71 quasi_webview::Webview::new().fragment(&act(route, filename))
72 }
73
74 #[cfg(test)]
75 mod tests {
76 /// The three things `data-action` + two positional arguments used to say,
77 /// said once. `data-saves` is the whole of the host's instruction.
78 #[test]
79 fn an_export_says_where_it_reads_from_and_what_the_file_is_called() {
80 let html = super::html("/api/export/sales", "sales.csv");
81
82 assert!(html.contains(r#"hx-post="/api/export/sales""#), "{html}");
83 assert!(html.contains(r#"data-saves="sales.csv""#), "{html}");
84 assert!(html.contains("Export CSV"), "{html}");
85 }
86
87 /// The mark that says the call waits. A file the server assembles is the
88 /// report case `Action::awaiting` exists for, and the button locking itself
89 /// is what the deleted global did by hand.
90 #[test]
91 fn an_export_says_it_waits() {
92 let html = super::html("/api/export/sales", "sales.csv");
93
94 assert!(html.contains("data-awaiting="), "{html}");
95 }
96
97 /// Nothing positional survives. The failure this replaced was a second
98 /// argument drifting from the first at one of five sites, silently.
99 #[test]
100 fn no_call_site_carries_a_behaviour_by_name() {
101 let html = super::html("/api/export/followers", "followers.csv");
102
103 assert!(!html.contains("data-action"), "{html}");
104 assert!(!html.contains("data-arg"), "{html}");
105 }
106 }
107