//! The Askama entry point for the two described removals on a reader's library.
//!
//! Shape 8 (`b279b9eb`), through the glue-module ruling `27d5e5b8`. Both
//! destroy a row and both ask.
//!
//! # Two spellings of "where the answer goes", and why they differ
//!
//! Neither `DELETE /api/collections/{id}` nor `POST /api/wishlists/{item}` is
//! served by the description layer, so each control names what it replaces. A
//! wishlist row has an id in the page already (`wishlist-row-{item}`) and says
//! it with [`Action::replacing`]. A collection row has none: the template
//! reached it with `closest tr`, which is the case
//! [`Action::replacing_enclosing`] was added for, and it needs no parameter
//! because the act is inside the row it removes. The row carries `data-row` in
//! Askama for the same reason a described row does.
//!
//! The wishlist row asked for `hx-swap="outerHTML swap:0.2s"`, and every
//! described act swaps `outerMorph` with no delay, so that row stops playing
//! the global `.htmx-swapping` fade. A swap timing is a fact about one
//! renderer, which is exactly the kind of thing a description does not hold;
//! every other described control in the app already lost it.
//!
//! Inventing an id for the collection row was the other option and is the wrong
//! one: `version_delete_act` minted one because a version row has an identity
//! in the database the template was simply not writing down, and a `
` in a
//! list of collections has no such fact going spare.
//!
//! Ids arrive as `impl Display` rather than `&str`: the caller is Askama and
//! the server's ids are typed newtypes over `Uuid`, so asking for a string
//! would push a `to_string()` into every call site to say what `format!`
//! already says here.
use std::fmt;
use makeover_layout::Tone;
use quasi_router::screen::Act;
use quasi_router::{Action, Node};
/// Delete one collection, replacing the row it sits in.
#[must_use]
pub fn delete_collection_html(collection_id: impl fmt::Display) -> String {
fragment(
Act::new(
"Delete",
Action::delete(format!("/api/collections/{collection_id}")).replacing_enclosing(),
)
.tone(Tone::Danger)
.confirm("Delete this collection?"),
)
}
/// Take one item off the wishlist.
///
/// A post rather than a delete, because the route toggles: the same address
/// puts an item on the list. The description says what this press does, which
/// is what the reader is being asked about.
#[must_use]
pub fn remove_from_wishlist_html(item_id: impl fmt::Display) -> String {
fragment(
Act::new(
"Remove",
Action::post(format!("/api/wishlists/{item_id}"))
.replacing(format!("wishlist-row-{item_id}")),
)
.tone(Tone::Danger)
.confirm("Remove from wishlist?"),
)
}
/// No shell and no region: this lands inside a document Askama has already
/// built, which is what every glue module here does.
fn fragment(act: Act) -> String {
use quasi_axum::Serves as _;
quasi_webview::Webview::new().fragment(&Node::Act(act))
}
#[cfg(test)]
mod tests {
/// The row it sits in, rather than an id the table would have had to grow.
#[test]
fn deleting_a_collection_replaces_the_row_it_is_in() {
let html = super::delete_collection_html("c1");
assert!(
html.contains(r#"hx-delete="/api/collections/c1""#),
"{html}"
);
assert!(html.contains(r#"hx-target="closest [data-row]""#), "{html}");
assert!(
html.contains(r#"hx-confirm="Delete this collection?""#),
"{html}"
);
}
#[test]
fn removing_from_the_wishlist_names_the_row_the_page_already_has() {
let html = super::remove_from_wishlist_html("i7");
assert!(html.contains(r#"hx-post="/api/wishlists/i7""#), "{html}");
assert!(html.contains(r##"hx-target="#wishlist-row-i7""##), "{html}");
assert!(
html.contains(r#"hx-confirm="Remove from wishlist?""#),
"{html}"
);
}
#[test]
fn both_removals_say_they_are_dangerous() {
assert!(super::delete_collection_html("c1").contains(r#"data-tone="danger""#));
assert!(super::remove_from_wishlist_html("i7").contains(r#"data-tone="danger""#));
}
}