|
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 not here, and that is measured
|
|
36 |
+ |
//!
|
|
37 |
+ |
//! Wiki [[mnw-shape-conversion-plans]] Shape 3 step 4 says
|
|
38 |
+ |
//! `download-version-btn` is `Action::get`. It is not.
|
|
39 |
+ |
//! `GET /api/versions/{id}/download` (`routes::storage::downloads`) answers
|
|
40 |
+ |
//! **JSON carrying a presigned URL**, and the handler then navigates to it, so
|
|
41 |
+ |
//! a described `Action::get` would send the reader to a JSON body. Making it
|
|
42 |
+ |
//! sayable is a route change — answer a redirect, or serve the route through
|
|
43 |
+ |
//! the description layer so it can answer `Outcome::Goto` with a
|
|
44 |
+ |
//! `Destination::External` — rather than a control change, so the handler
|
|
45 |
+ |
//! stays.
|
|
46 |
+ |
|
|
47 |
+ |
use makeover_layout::Tone;
|
|
48 |
+ |
use quasi_router::screen::Act;
|
|
49 |
+ |
use quasi_router::{Action, Node};
|
|
50 |
+ |
|
|
51 |
+ |
/// The delete button for one version's row.
|
|
52 |
+ |
///
|
|
53 |
+ |
/// Both ids are needed and they are not the same fact: the route is addressed
|
|
54 |
+ |
/// by item and version together, and the row is named by the version alone.
|
|
55 |
+ |
#[must_use]
|
|
56 |
+ |
pub fn act(item_id: &str, version_id: &str) -> Node {
|
|
57 |
+ |
Node::Act(
|
|
58 |
+ |
Act::new(
|
|
59 |
+ |
"Delete",
|
|
60 |
+ |
Action::delete(format!("/api/items/{item_id}/versions/{version_id}"))
|
|
61 |
+ |
.replacing(format!("version-row-{version_id}")),
|
|
62 |
+ |
)
|
|
63 |
+ |
.tone(Tone::Danger)
|
|
64 |
+ |
.confirm("Delete this version?"),
|
|
65 |
+ |
)
|
|
66 |
+ |
}
|
|
67 |
+ |
|
|
68 |
+ |
/// The same act as a fragment, for the Askama call site.
|
|
69 |
+ |
#[must_use]
|
|
70 |
+ |
pub fn html(item_id: &str, version_id: &str) -> String {
|
|
71 |
+ |
use quasi_axum::Serves as _;
|
|
72 |
+ |
|
|
73 |
+ |
quasi_webview::Webview::new().fragment(&act(item_id, version_id))
|
|
74 |
+ |
}
|
|
75 |
+ |
|
|
76 |
+ |
#[cfg(test)]
|
|
77 |
+ |
mod tests {
|
|
78 |
+ |
/// What the click handler said in four places, said once: where it goes,
|
|
79 |
+ |
/// what it replaces, and what to ask first.
|
|
80 |
+ |
#[test]
|
|
81 |
+ |
fn a_delete_names_its_route_its_row_and_its_prompt() {
|
|
82 |
+ |
let html = super::html("7", "42");
|
|
83 |
+ |
|
|
84 |
+ |
assert!(
|
|
85 |
+ |
html.contains(r#"hx-delete="/api/items/7/versions/42""#),
|
|
86 |
+ |
"{html}"
|
|
87 |
+ |
);
|
|
88 |
+ |
assert!(html.contains(r##"hx-target="#version-row-42""##), "{html}");
|
|
89 |
+ |
assert!(
|
|
90 |
+ |
html.contains(r#"hx-confirm="Delete this version?""#),
|
|
91 |
+ |
"{html}"
|
|
92 |
+ |
);
|
|
93 |
+ |
}
|
|
94 |
+ |
|
|
95 |
+ |
/// The tone. The template said nothing at all here -- the button was
|
|
96 |
+ |
/// `btn-secondary`, so the page did not mark the destructive act even to
|
|
97 |
+ |
/// the stylesheet.
|
|
98 |
+ |
#[test]
|
|
99 |
+ |
fn a_delete_says_it_is_dangerous_in_a_word_every_renderer_reads() {
|
|
100 |
+ |
let html = super::html("7", "42");
|
|
101 |
+ |
|
|
102 |
+ |
assert!(html.contains(r#"data-tone="danger""#), "{html}");
|
|
103 |
+ |
}
|
|
104 |
+ |
|
|
105 |
+ |
/// The negative half, as `export_act` asserts it: the dispatcher is gone
|
|
106 |
+ |
/// from this control, not merely bypassed.
|
|
107 |
+ |
#[test]
|
|
108 |
+ |
fn nothing_here_goes_through_the_dispatcher() {
|
|
109 |
+ |
let html = super::html("7", "42");
|
|
110 |
+ |
|
|
111 |
+ |
assert!(!html.contains("data-action"), "{html}");
|
|
112 |
+ |
assert!(!html.contains("delete-version-btn"), "{html}");
|
|
113 |
+ |
}
|
|
114 |
+ |
}
|