Skip to main content

max / makenotwork

5.4 KB · 126 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_declare::declare;
43
44 declare! {
45 /// The route each export posts to, and the name the file is kept under.
46 ///
47 /// A read would be a link the browser downloads by itself; these are posts,
48 /// because assembling the file is work the server does rather than a document
49 /// it already has, and `Action::saves` is independent of the verb for exactly
50 /// that reason.
51 ///
52 /// `awaiting` because the whole file is assembled before any of it comes back.
53 /// Nothing countable to say about the wait: the row count is often known at the
54 /// call site and the byte count never is, and `layout::Awaiting` takes a
55 /// measurement rather than a stand-in for one.
56 #[must_use]
57 #[constant]
58 pub shape act(route: &str, filename: &str) -> Node;
59
60 include control("Export CSV", route, filename);
61 }
62
63 declare! {
64 /// The same sentence under a caller's own label.
65 ///
66 /// `Export CSV` is right for a control standing alone among others, which is
67 /// every site [`act`] serves. It is wrong on the export portal, where six cards
68 /// each name their own subject and the format is already in the card's meta
69 /// line, so the control there reads `Download`. The label is the only thing
70 /// that varies: what a caller must not restate is the sentence, which is here
71 /// once and is what this module exists for.
72 ///
73 /// Returns the [`Act`] rather than a [`Node`] because the portal's controls sit
74 /// in a [`quasi_router::Row`], which holds acts rather than nodes.
75 #[must_use]
76 #[constant]
77 pub shape control(label: &str, route: &str, filename: &str) -> Act;
78
79 act label to post route saving filename awaiting;
80 }
81
82 /// The same act as a fragment, for a template to drop in place.
83 ///
84 /// No shell and no region: this lands inside a document Askama has already
85 /// built, which is what every glue module here does.
86 #[must_use]
87 pub fn html(route: &str, filename: &str) -> String {
88 use quasi_axum::Serves as _;
89
90 quasi_webview::Webview::new().fragment(&act(route, filename))
91 }
92
93 #[cfg(test)]
94 mod tests {
95 /// The three things `data-action` + two positional arguments used to say,
96 /// said once. `data-saves` is the whole of the host's instruction.
97 #[test]
98 fn an_export_says_where_it_reads_from_and_what_the_file_is_called() {
99 let html = super::html("/api/export/sales", "sales.csv");
100
101 assert!(html.contains(r#"hx-post="/api/export/sales""#), "{html}");
102 assert!(html.contains(r#"data-saves="sales.csv""#), "{html}");
103 assert!(html.contains("Export CSV"), "{html}");
104 }
105
106 /// The mark that says the call waits. A file the server assembles is the
107 /// report case `Action::awaiting` exists for, and the button locking itself
108 /// is what the deleted global did by hand.
109 #[test]
110 fn an_export_says_it_waits() {
111 let html = super::html("/api/export/sales", "sales.csv");
112
113 assert!(html.contains("data-awaiting="), "{html}");
114 }
115
116 /// Nothing positional survives. The failure this replaced was a second
117 /// argument drifting from the first at one of five sites, silently.
118 #[test]
119 fn no_call_site_carries_a_behaviour_by_name() {
120 let html = super::html("/api/export/followers", "followers.csv");
121
122 assert!(!html.contains("data-action"), "{html}");
123 assert!(!html.contains("data-arg"), "{html}");
124 }
125 }
126