Skip to main content

max / makenotwork

4.3 KB · 107 lines History Blame Raw
1 //! The Askama entry point for a described "delete this version" button.
2 //!
3 //! Shape 3 step 4 (`108557ec`), the step the plan calls the clean tail: the one
4 //! part of `static/item-upload.js` that retires with no host remnant. It waited
5 //! on `27d5e5b8` (a described act inside an Askama page) and on `35756077` (the
6 //! unlayered `button` rule taking the tone off every described act). Both are
7 //! settled, so it converts.
8 //!
9 //! # What the description says that the markup did not
10 //!
11 //! Everything, in this case. There was no markup: the button carried a class
12 //! and a `data-version-id`, and the whole act lived in a click handler in
13 //! `item-upload.js` — a `confirm()` string, a `fetch` with CSRF headers, and a
14 //! `btn.closest('tr').remove()` on success. None of that is legible to anything
15 //! but a browser. As an [`Act`] it is one sentence: what it is called, where it
16 //! goes, what it replaces, that it is destructive, and what to ask first.
17 //!
18 //! # Why it names what it replaced, and why the spelling changed
19 //!
20 //! `DELETE /api/items/{item}/versions/{version}` is a plain API route the
21 //! description layer does not serve; it answers `200` with an empty body, so it
22 //! cannot name what it changed and the control has to. See
23 //! [`Action::replaces`].
24 //!
25 //! It said `#version-row-{id}`, an id the Askama table grew in the same change
26 //! that landed this module, because the handler it replaced reached the row
27 //! through `closest('tr')`. The table is described now (`super::item_files`,
28 //! `138ad5ab`) and a described row carries `data-row` and no id at all, so an
29 //! id target would name nothing. [`Action::replacing_enclosing`] is the same
30 //! sentence in the spelling a described row understands, and it needs no
31 //! parameter: the act is inside the row it removes.
32 //!
33 //! # The download button beside it is sayable now, and was not when this landed
34 //!
35 //! `download-version-btn` is `Action::get`. `GET /api/versions/{id}/download`
36 //! (`routes::storage::downloads`) answers **303 to the presigned URL**, so
37 //! navigating to it is the download and `Action::get` says the truth. The button
38 //! is a plain navigation in `static/item-upload.js` today and becomes an act
39 //! whenever the files tab converts.
40
41 use quasi_declare::declare;
42
43 declare! {
44 /// The delete button for one version's row.
45 ///
46 /// Both ids are needed for the address, which is item and version together.
47 /// The row it removes is the one it sits in, so nothing has to name it.
48 #[must_use]
49 pub shape act(item_id: &str, version_id: &str) -> Act;
50
51 act "Delete" to delete "/api/items/{item_id}/versions/{version_id}" replacing_enclosing {
52 tone Danger;
53 confirm "Delete this version?";
54 }
55 }
56
57 #[cfg(test)]
58 mod tests {
59 use quasi_router::Node;
60
61 /// What the click handler said in four places, said once: where it goes,
62 /// what it replaces, and what to ask first.
63 ///
64 /// Rendered through a bare `Node::Act` rather than through the panel: this
65 /// module owns the sentence, and `super::item_files` asserts it arrives in
66 /// the row.
67 fn html() -> String {
68 use quasi_axum::Serves as _;
69
70 quasi_webview::Webview::new().fragment(&Node::Act(super::act("7", "42")))
71 }
72
73 #[test]
74 fn a_delete_names_its_route_its_row_and_its_prompt() {
75 let html = html();
76
77 assert!(
78 html.contains(r#"hx-delete="/api/items/7/versions/42""#),
79 "{html}"
80 );
81 // The row it sits in, rather than an id the table had to grow.
82 assert!(!html.contains("version-row-42"), "{html}");
83 assert!(
84 html.contains(r#"hx-confirm="Delete this version?""#),
85 "{html}"
86 );
87 }
88
89 /// The tone. The template said nothing at all here -- the button was
90 /// `btn-secondary`, so the page did not mark the destructive act even to
91 /// the stylesheet.
92 #[test]
93 fn a_delete_says_it_is_dangerous_in_a_word_every_renderer_reads() {
94 assert!(html().contains(r#"data-tone="danger""#));
95 }
96
97 /// The negative half, as `export_act` asserts it: the dispatcher is gone
98 /// from this control, not merely bypassed.
99 #[test]
100 fn nothing_here_goes_through_the_dispatcher() {
101 let html = html();
102
103 assert!(!html.contains("data-action"), "{html}");
104 assert!(!html.contains("delete-version-btn"), "{html}");
105 }
106 }
107