//! 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""#)); } }