Skip to main content

max / makenotwork

4.9 KB · 117 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 the row id is a parameter, and why the row grew one
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 //! The blog pair ([`super::blog_delete_act`]) converted because its templates
26 //! already gave each row a real id. This one's `<tr>` did not, and the handler
27 //! it replaces reached the row through `closest('tr')`. So the template grew
28 //! `id="version-row-{id}"` in the same change. That is worth telling apart from
29 //! the two rows that are still hand-written: `partials/link_row.html` and
30 //! `partials/tag.html` target `closest .<class>` on rows whose identity is a
31 //! CSS class and nothing else, and inventing an id for them would be describing
32 //! a fact the page does not have. A version row has an id in the database and
33 //! the template was simply not writing it down.
34 //!
35 //! # The download button beside it is sayable now, and was not when this landed
36 //!
37 //! Wiki [[mnw-shape-conversion-plans]] Shape 3 step 4 said
38 //! `download-version-btn` is `Action::get`. When this module landed that was
39 //! wrong: `GET /api/versions/{id}/download` (`routes::storage::downloads`)
40 //! answered JSON carrying a presigned URL and the handler navigated to it, so a
41 //! described `Action::get` would have sent the reader to a JSON body.
42 //!
43 //! Fixed 2026-08-26 by the route rather than by the control (`8fc6b1af`,
44 //! option (a)): it answers **303 to the presigned URL**, so navigating to it is
45 //! the download and `Action::get` says the truth. The button is a plain
46 //! navigation in `static/item-upload.js` today and becomes an act whenever the
47 //! files tab converts. Nothing blocks it any more.
48
49 use makeover_layout::Tone;
50 use quasi_router::screen::Act;
51 use quasi_router::{Action, Node};
52
53 /// The delete button for one version's row.
54 ///
55 /// Both ids are needed and they are not the same fact: the route is addressed
56 /// by item and version together, and the row is named by the version alone.
57 #[must_use]
58 pub fn act(item_id: &str, version_id: &str) -> Node {
59 Node::Act(
60 Act::new(
61 "Delete",
62 Action::delete(format!("/api/items/{item_id}/versions/{version_id}"))
63 .replacing(format!("version-row-{version_id}")),
64 )
65 .tone(Tone::Danger)
66 .confirm("Delete this version?"),
67 )
68 }
69
70 /// The same act as a fragment, for the Askama call site.
71 #[must_use]
72 pub fn html(item_id: &str, version_id: &str) -> String {
73 use quasi_axum::Serves as _;
74
75 quasi_webview::Webview::new().fragment(&act(item_id, version_id))
76 }
77
78 #[cfg(test)]
79 mod tests {
80 /// What the click handler said in four places, said once: where it goes,
81 /// what it replaces, and what to ask first.
82 #[test]
83 fn a_delete_names_its_route_its_row_and_its_prompt() {
84 let html = super::html("7", "42");
85
86 assert!(
87 html.contains(r#"hx-delete="/api/items/7/versions/42""#),
88 "{html}"
89 );
90 assert!(html.contains(r##"hx-target="#version-row-42""##), "{html}");
91 assert!(
92 html.contains(r#"hx-confirm="Delete this version?""#),
93 "{html}"
94 );
95 }
96
97 /// The tone. The template said nothing at all here -- the button was
98 /// `btn-secondary`, so the page did not mark the destructive act even to
99 /// the stylesheet.
100 #[test]
101 fn a_delete_says_it_is_dangerous_in_a_word_every_renderer_reads() {
102 let html = super::html("7", "42");
103
104 assert!(html.contains(r#"data-tone="danger""#), "{html}");
105 }
106
107 /// The negative half, as `export_act` asserts it: the dispatcher is gone
108 /// from this control, not merely bypassed.
109 #[test]
110 fn nothing_here_goes_through_the_dispatcher() {
111 let html = super::html("7", "42");
112
113 assert!(!html.contains("data-action"), "{html}");
114 assert!(!html.contains("delete-version-btn"), "{html}");
115 }
116 }
117